Skip to content

Instantly share code, notes, and snippets.

@joevt
Last active February 8, 2024 00:44
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save joevt/e3cd4ff08aae06279134969c98ca3ab7 to your computer and use it in GitHub Desktop.
Save joevt/e3cd4ff08aae06279134969c98ca3ab7 to your computer and use it in GitHub Desktop.
A bash script to produce more informative output than lspci -nntv
#!/bin/bash
# by joevt Feb 6, 2024
# Jan 22, 2023 - Updated to work in Mac OS X 10.4.
# Jan 30, 2023 - Fixed the method options.
# Apr 7, 2023 - Fixed parse of unknown prog-if and move temp files to separate tmp directory.
# Feb 6, 2024 - Fix column alignment.
#===================
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root like this:"
echo "sudo $0"
exit 1
fi
#===================
style="wide"
style="narrow"
#encoding="ascii"
encoding="utf8"
method="" # default method which for macOS on Intel is -Adarwin
#method="-Adarwin" # uses AppleACPIPlatformExpert user client
#method="-Adarwin2" # uses IOPCIBridge user client
#method="-Adarwin3" # uses IOPCIBridge through DirectHW user client
#method="-Aintel-conf1" # uses I/O to CF8h/CFCh (requires Intel DirectHW.kext on macOS)
#method="-H1" # same as -Aintel-conf1
#===================
if [ $encoding == "ascii" ]; then
box0='\'
box1='-'
box2='\'
box3='+'
box4='|'
box5='.'
else
box0='┐'
box1='─'
box2='└'
box3='├'
box4='│'
box5='┬'
fi
if [ $style == "wide" ]; then
if [ $encoding == "ascii" ]; then
# since we're using \, we want to indent one extra space
rootindent=' '
subindent=' '
else
rootindent=' '
subindent=' '
fi
domainprefix="\\${box1}\\["
domainsuffix="\\]\\${box1}\\${box0}"
bridgeprefix="\\${box3}\\${box1}"
bridgesuffixlong="\\${box1}\\${box1}\\${box1}\\${box1}\\${box0}"
bridgesuffix="\\${box1}\\${box0}"
deviceprefix="\\${box3}\\${box1}"
devicesuffix=""
else
rootindent=''
subindent=' '
domainprefix="\\${box5}\\["
domainsuffix="\\]"
bridgeprefix="\\${box3}\\${box5}"
bridgesuffixlong=""
bridgesuffix=""
deviceprefix="\\${box3}\\${box1}"
devicesuffix=""
fulladdress=1
fi
#===================
#tmp=/tmp/pcitree
tmp="$(mktemp -d "/tmp/pcitree.XXXX")"
mkdir -p "$tmp"
chmod 777 "$tmp"
# Get header type and bus information for all pci devices - reverse sort so last bus is first (required for device path substitution step below).
# Format is "domain:bus%device.function domain:primary domain:secondary domain:subordinate".
# Keep the domain number for device path substitution step below.
# -0777 allows perl to process all lines at once - here we process two lines at a time.
setpci $method -v -s '*:*:*' HEADER_TYPE.b 0x18.l 2> /dev/null | perl -0777 -pe '
s/((.{4}):(..):(..\..)) \@0e = ([08][12])\n\1 \@18 = ..(.{2})(.{2})(.{2})\n/$2:$3%$4 $2:$8 $2:$7 $2:$6\n/g; # bridge(01) or card bus(02) header type, multifunction(80) or not (00).
s/((.{4}):(..):(..\..)) \@0e = (..)\n\1 \@18 = ..(.{2})(.{2})(.{2})\n/$2:$3%$4\n/g # endpoint (or anything else) header type
' \
| sort -r > "$tmp"/pcilist.txt
# If primary = secondary = subordinate then that is a problem - change them to XX.
sed '/^\(....:..%..\..\) \(\(.\{4\}:\)..\) \2 \2$/s//\1 \3XX \3XX \3XX/' "$tmp"/pcilist.txt > "$tmp"/pcilist2.txt
echo '/ ....:/s// /g' > "$tmp"/pcipaths.txt # Add a command that will remove domain from primary, secondary, and subordinate bus numbers.
# Create device path substitution commands that will add parent bus to each secondary bus.
# When applied to the list of pci devices, the commands will create a full device path for
# each pci device. Multiple commands will be applied to each device to build the device
# path from right to left: end point (greatest bus number) to domain (lowest bus number).
# The commands are sorted from highest bus number to lowest bus number to make that possible.
sed -nE '/^(....:..%..\..) .{4}:.. (.{4}:..) .*/s//\/(\2)\/s\/\/\1%\\1\/g/p' "$tmp"/pcilist2.txt >> "$tmp"/pcipaths.txt
sed -E -f "$tmp"/pcipaths.txt "$tmp"/pcilist2.txt > "$tmp"/pcitree1.txt # Apply the device path substitution commands.
sed -E '/^(....:..).*/s//\1/g' "$tmp"/pcitree1.txt | sort -u > "$tmp"/pcitree2.txt # Get all root complexes (usually just 0000:00).
cat "$tmp"/pcitree1.txt "$tmp"/pcitree2.txt | LC_ALL=C sort > "$tmp"/pcitree3.txt # Concatenate the devices and root complexes and sort.
# Use perl to do some formatting.
perl -CSDA -Mutf8 -pe '
s/^(.*(\w\w\w\w:\w\w%\w\w\.\w).*)/\1 # \2/; # save domain/bus/device/function after a " # " - this will be used to append name and ids
s/^\w\w\w\w:\w\w%/'"$rootindent"'/; # remove domain/bus number from left side and indent
s/\w\w\.\w%\w\w\w\w:\w\w%/'"$subindent"'/g; # use the paths to do more indenting
s/(\w\w\.\w) \w\w (\w\w) \2/'"${bridgeprefix}"'\1-[\2]'"${bridgesuffixlong}"'/; # format the secondary and subordinate bus numbers for single bus bridge
s/(\w\w\.\w) \w\w (\w\w) (\w\w)/'"${bridgeprefix}"'\1-[\2-\3]'"${bridgesuffix}"'/; # format the secondary and subordinate bus numbers for multiple bus bridge
s/%/:/; # replace % bus/device delimiter with standard ":"
s/(\w\w\.\w )/'"${deviceprefix}"'\1'"${devicesuffix}"'/; # add the "+-" formating for devices
s/^(\w\w\w\w:\w\w)$/'"${domainprefix}"'\1'"${domainsuffix}"'/ # add the -[ ]\" for domain/bus
' \
"$tmp"/pcitree3.txt > "$tmp"/pcitree4.txt
# https://perldoc.perl.org/perlrun.html
# The -C flag controls some of the Perl Unicode features.
# S: STDIN, STDOUT and STDERR are assumed to be in UTF-8.
# D: UTF-8 is the default PerlIO layer for both input and output streams.
# A: @ARGV elements are expected to be strings encoded in UTF-8.
# -0[octal/hexadecimal] specifies the input record separator ($/ ) as an octal or hexadecimal number.
# Any value 0400 or above will cause Perl to slurp files whole, but by convention the value 0777 is the one normally used for this purpose.
# -Mmodule executes use module ; before executing your program. This loads the module and calls its import method, causing the module to have its default effect, typically importing subroutines or giving effect to a pragma.
# -p causes Perl to assume a loop around your program, which makes it iterate over filename arguments somewhat like sed.
# -e commandline May be used to enter one line of program.
perl -CSDA -Mutf8 -0777 -pe '
# add | between devices of the same bus
do {
$good=0;
while (
/ # begin regular expression
( # begin g1
\n # \n
([\ '"\\${box4}"']*) # [ |]* -> g2
['"\\${box3}\\${box4}"'] # [+|]
[^\n]* # rest of the line up to \n, (change + to *+ to give nothing back)
(?{$X=pos()}) # mark position
\n # \n
) # end g1
(?= # begin lookahead
( # begin group
\2\ [^\n]* # following line(s) start with g2 also (change + to *+ to give nothing back)
\n #
)+ # end group, match one or more, (change + to ++ to give nothing back)
\2 # the line after those start with g2
'"\\${box3}"' # and "+" meaning that another device follows g1 on the same bus
) # end lookahead
(
\2 #
(?{$C=pos()}) # $C is the position where we want to insert the |
\ # the character at position $C to be modified is a space character
) #
/gx # end regular expression (g=global; x=ignore white space in RE)
) {
$good = 1;
substr($_,$C,1) = "'"\\${box4}"'"; # replace the space character at position $C with a |
pos() = $X; # start next search at $X which is the end of the previous line
}
} until ($good == 0);
# replace + with \ for last device of a bus
s/(\n[\ '"\\${box4}"']*)'"\\${box3}"'(?![^\n]*\1['"\\${box3}\\${box4}"'])/\1'"\\${box2}"'/g;
' "$tmp"/pcitree4.txt > "$tmp"/pcitree5.txt
# Create the substitutions for the detail column
lspci $method -nnv -D 2> /dev/null > "$tmp"/pcidevices0_lspci.txt
perl -pe '
s/^\n//g;
s/^[ \t].*\n//g;
s|/|\\/|g;
s|^(....:..:..\..) ([^[]*) (\[....\])(.*) (\[....:....\])( \(rev ..\))?(?: \(prog-if ..(( \[.*?\])?)\))?|/# (\1)\$/s//'" "'# \\1 \5 \3:\6 : \2\7 \4/|;
s/\]:([^:]{9}.*?) *:([^:]{37}.*?) *:/]\1\2 :/ # rev column is 9 characters and type column is 36 characters
' \
"$tmp"/pcidevices0_lspci.txt > "$tmp"/pcidevices1_lspci_sed.txt
# Get link width and speed max/current.
pat="(....:..:..\..) @09 = (..)"
IFS=$'\n'
echo "" > "$tmp"/pcidevices2_link_sed.txt
echo "" > "$tmp"/pcidevices1_progintf_sed.txt
for thedevice in $(sudo setpci $method -v -s '*:*:*' CLASS_PROG.b 2> /dev/null); do
pcidevice="${thedevice% @*}"
proginterface="${thedevice#* = }"
# We have to do this in a loop because setpci stops if express capability doesn't exist
LinkRegisters=$(sudo setpci $method -s $pcidevice CAP_EXP+12.w CAP_EXP+c.l 2> /dev/null)
if [[ -n $LinkRegisters ]]; then
LinkStatusRegister=${LinkRegisters:0:4}
LinkWidth=$((0x$LinkStatusRegister >> 4 & 31))
if [[ $LinkWidth -ge 0 ]]; then
LinkSpeed=$((0x$LinkStatusRegister & 15))
LinkCapabilitiesRegister=${LinkRegisters:5:8}
MaxLinkWidth=$((0x$LinkCapabilitiesRegister >> 4 & 31))
MaxLinkSpeed=$((0x$LinkCapabilitiesRegister & 15))
echo "/# ($pcidevice .*$)/s//# g${MaxLinkSpeed}x${MaxLinkWidth} > g${LinkSpeed}x${LinkWidth} \1/" >> "$tmp"/pcidevices2_link_sed.txt
fi
fi
echo "/(# +$pcidevice \[....:....\] \[....)/s//\1${proginterface}/" >> "$tmp"/pcidevices1_progintf_sed.txt
done
perl -CSDA -Mutf8 -pe '
s/# (g.x..?) > \1 /# \1 /; # if the links are the same then output it only once
s|(/s//#.{16}) *|\1|; # make the link column 16 characters wide
' "$tmp"/pcidevices2_link_sed.txt > "$tmp"/pcidevices3_link_sed.txt
# If there's only one domain/segment then remove them from the output.
if [[ $(sed -E '/(....).*/s//\1/' "$tmp"/pcitree2.txt | sort -u | wc -l) -eq 1 ]]; then
echo "/(#.{16}).{4}:/s//\\1/" >> "$tmp"/pcidevices3_link_sed.txt
fi
if [[ $fulladdress -eq 1 ]]; then
echo "/..\..([^#]*#.{16})([0-9a-f.:]+) /s//\\2\\1/" >> "$tmp"/pcidevices3_link_sed.txt
fi
# Apply the substitutions
sed -E -f "$tmp"/pcidevices1_lspci_sed.txt -f "$tmp"/pcidevices1_progintf_sed.txt -f "$tmp"/pcidevices3_link_sed.txt "$tmp"/pcitree5.txt > "$tmp"/pcitree6.txt
# Set width of the first column to the minimum required
spaces=$(perl -CSDA -Mutf8 -e '$spaces=0; while (<>) { if (/^([^#]*?) *#/) { $s = length($1); if ($s > $spaces) { $spaces = $s }}} print $spaces' "$tmp"/pcitree6.txt)
perl -CSDA -Mutf8 -pe 's/^(.{'"$((spaces+2))"'}) */$1/' "$tmp"/pcitree6.txt > "$tmp"/pcitree7.txt
echo '#========================================================================================='
cat "$tmp"/pcitree7.txt # final output
@joevt
Copy link
Author

joevt commented Jul 18, 2021

I created a EFI driver called FixPCIeLinkRate.efi which also produces similar output to pcitree.sh. https://forums.macrumors.com/threads/opencore-and-the-2008-mac-pro-3-1.2287044/post-30087837
https://github.com/joevt/joevtApps

@joevt
Copy link
Author

joevt commented Dec 14, 2022

Updates

Feb 6, 2024

  • Fix column alignment.

Apr 7, 2023

  • Fixed parse of unknown prog-if
  • Moved temporary files to separate tmp directory.

Jan 30, 2023

  • Fixed list of alternate access methods.

Jan 22, 2023

  • Updated to work in Mac OS X 10.4. Now it can be used for PowerPC Macs.

Jan 3, 2023

  • Remove domain numbers if they're all the same.

Dec 14, 2022

  • Updated for latest pciutils.
  • Includes program interface in the class code columns (the numeric column and the text column).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment