Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c56c005

Browse files
authoredApr 28, 2025··
Add script to update README with output from help commands (#220)
This will update all the `picotool help ...` sections in the README with the current output * Add workflow to check help text is up-to-date * Add a check that all commands have help text in the readme
1 parent f010190 commit c56c005

File tree

5 files changed

+612
-60
lines changed

5 files changed

+612
-60
lines changed
 

‎.github/workflows/check_help_text.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Check Help Text
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
check-help-text:
9+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Install dependencies (Linux)
16+
if: runner.os == 'Linux'
17+
run: sudo apt install cmake ninja-build python3 build-essential libusb-1.0-0-dev
18+
19+
- name: Checkout Pico SDK
20+
uses: actions/checkout@v4
21+
with:
22+
repository: raspberrypi/pico-sdk
23+
ref: develop
24+
path: pico-sdk
25+
submodules: 'true'
26+
27+
- name: Build and Install
28+
run: |
29+
cmake -S . -B build -G "Ninja" -D PICO_SDK_PATH="${{ github.workspace }}/pico-sdk" -D GENERATE_FIXED_DOCS_WIDTH=1
30+
cmake --build build
31+
sudo cmake --install build
32+
33+
- name: Check if help text needs updating
34+
run: |
35+
# Create a copy of README.md
36+
cp README.md README.md.orig
37+
38+
# Run the script
39+
./gen_help_txt.sh
40+
41+
# Compare the files
42+
if ! cmp -s README.md README.md.orig; then
43+
echo "Error: Help text in README.md is out of date - update by running gen_help_txt.sh, or use the artifact from this workflow"
44+
exit 1
45+
fi
46+
47+
# Clean up
48+
rm README.md.orig
49+
50+
- name: Upload new README.md
51+
if: always()
52+
uses: actions/upload-artifact@v4
53+
with:
54+
name: README.md
55+
path: README.md

‎CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ else()
288288
${LIBUSB_LIBRARIES})
289289
endif()
290290

291+
if (GENERATE_FIXED_DOCS_WIDTH)
292+
target_compile_definitions(picotool PRIVATE DOCS_WIDTH=140)
293+
endif()
294+
291295
# allow `make install`
292296
install(TARGETS picotool
293297
EXPORT picotool-targets

‎README.md

Lines changed: 409 additions & 33 deletions
Large diffs are not rendered by default.

‎gen_help_txt.sh

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/bin/bash
2+
3+
ALLOWED_MISSING_COMMANDS="version"
4+
5+
mkdir -p tmp
6+
cp README.md tmp/README.md
7+
8+
# Update each help text section with the current text
9+
while IFS= read -r line; do
10+
if [[ $line =~ ^\$[[:space:]]*picotool[[:space:]]+help ]]; then
11+
# Extract the commands
12+
cmd=$(echo "$line" | sed 's/^\$[[:space:]]*//')
13+
if [ ! -z "$cmd" ]; then
14+
echo "Running: $cmd"
15+
$cmd > "tmp/${cmd// /_}.txt"
16+
17+
# Create the new text section
18+
{
19+
printf '```text\n%s\n' "$line"
20+
cat "tmp/${cmd// /_}.txt"
21+
printf '```'
22+
} > "tmp/new_section.txt"
23+
24+
# Replace the old section with the new one
25+
escaped_line=$(echo "$line" | sed 's/[.*+?^${}()|[]/\\&/g')
26+
perl -i -pe '
27+
BEGIN { $/ = undef; $new = `cat tmp/new_section.txt`; }
28+
s/```text\n'"$escaped_line"'\n.*?\n```/$new/s;
29+
' tmp/README.md
30+
fi
31+
fi
32+
done < README.md
33+
34+
35+
# Extract commands to check for missing help text
36+
echo "Extracting commands from main.cpp..."
37+
38+
extract_commands() {
39+
local array_name=$1
40+
local prefix=""
41+
if [[ $array_name == *"_sub_commands" ]]; then
42+
prefix=${array_name%_sub_commands}
43+
fi
44+
sed -n "/vector<std::shared_ptr<cmd>> $array_name/,/};/p" main.cpp | grep -o 'new \([a-z0-9_]*\)_command' | cut -d' ' -f2 | sed 's/_command$//' | while read cmd; do
45+
if [[ ! -z "$prefix" ]]; then
46+
# Remove prefix from command and replace remaining underscores with dashes
47+
local cmd_without_prefix=${cmd#${prefix}_}
48+
echo "${prefix} ${cmd_without_prefix//_/-}"
49+
else
50+
# Replace underscores with dashes for main commands
51+
echo "${cmd//_/-}"
52+
fi
53+
done
54+
}
55+
56+
# Extract top level commands
57+
main_commands=$(extract_commands "commands")
58+
59+
# Extract sub-commands
60+
sub_command_arrays=$(grep -Eo 'vector<std::shared_ptr<cmd>> [a-z0-9_]+_sub_commands' main.cpp | cut -d' ' -f2)
61+
sub_commands=""
62+
for array in $sub_command_arrays; do
63+
echo "Extracting commands from $array..."
64+
array_commands=$(extract_commands "$array")
65+
sub_commands+=$'\n'"$array_commands"
66+
done
67+
68+
# Combine all commands
69+
commands=$(echo -e "$main_commands$sub_commands")
70+
71+
echo "Checking for missing help text sections..."
72+
missing_commands=()
73+
while IFS= read -r cmd; do
74+
if [ ! -z "$cmd" ]; then
75+
if ! grep -q "\$ picotool help $cmd" tmp/README.md; then
76+
if [[ ! "$ALLOWED_MISSING_COMMANDS" =~ "$cmd" ]]; then
77+
missing_commands+=("$cmd")
78+
echo "Missing help text section for command: $cmd"
79+
fi
80+
fi
81+
fi
82+
done <<< "$commands"
83+
84+
# If there are missing commands, add their help text sections to the end of the file
85+
# Still fails the job later, this is just to provide the output to copy into the right place
86+
if [ ${#missing_commands[@]} -ne 0 ]; then
87+
echo "Adding missing help text sections to end of file..."
88+
for cmd in "${missing_commands[@]}"; do
89+
echo "Running: picotool help $cmd"
90+
picotool help $cmd > "tmp/picotool_help_${cmd// /_}.txt"
91+
92+
{
93+
printf '\n```text\n$ picotool help %s\n' "$cmd"
94+
cat "tmp/picotool_help_${cmd// /_}.txt"
95+
printf '```\n'
96+
} >> tmp/README.md
97+
done
98+
fi
99+
100+
mv tmp/README.md README.md
101+
rm -rf tmp
102+
103+
echo "Help text sections have been updated in README.md"
104+
105+
# Still fail if there are missing commands
106+
if [ ${#missing_commands[@]} -ne 0 ]; then
107+
echo "Failing job due to missing help text sections"
108+
exit 1
109+
fi

‎main.cpp

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ auto device_selection =
524524
(option("--pid") & integer("pid").set(settings.pid)) % "Filter by product id" +
525525
(option("--ser") & value("ser").set(settings.ser)) % "Filter by serial number"
526526
+ option('f', "--force").set(settings.force) % "Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the command (unless the command itself is a 'reboot') the device will be rebooted back to application mode" +
527-
option('F', "--force-no-reboot").set(settings.force_no_reboot) % "Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the command (unless the command itself is a 'reboot') the device will be left connected and accessible to picotool, but without the RPI-RP2 drive mounted"
527+
option('F', "--force-no-reboot").set(settings.force_no_reboot) % "Force a device not in BOOTSEL mode but running compatible code to reset so the command can be executed. After executing the command (unless the command itself is a 'reboot') the device will be left connected and accessible to picotool, but without the USB drive mounted"
528528
).min(0).doc_non_optional(true).collapse_synopsys("device-selection");
529529

530530
#define file_types_x(i)\
@@ -668,15 +668,15 @@ struct verify_command : public cmd {
668668

669669
group get_cli() override {
670670
return (
671-
device_selection % "Target device selection" +
672671
file_selection % "The file to compare against" +
673672
(
674673
(option('r', "--range").set(settings.range_set) % "Compare a sub range of memory only" &
675674
hex("from").set(settings.from) % "The lower address bound in hex" &
676675
hex("to").set(settings.to) % "The upper address bound in hex").force_expand_help(true) +
677676
(option('o', "--offset").set(settings.offset_set) % "Specify the load address when comparing with a BIN file" &
678677
hex("offset").set(settings.offset) % "Load offset (memory address; default 0x10000000)").force_expand_help(true)
679-
).min(0).doc_non_optional(true) % "Address options"
678+
).min(0).doc_non_optional(true) % "Address options" +
679+
device_selection % "Target device selection"
680680
);
681681
}
682682

@@ -704,8 +704,8 @@ struct save_command : public cmd {
704704
(option("--family") % "Specify the family ID to save the file as" &
705705
family_id("family_id").set(settings.family_id) % "family ID to save file as").force_expand_help(true) +
706706
( // note this parenthesis seems to help with error messages for say save --foo
707-
device_selection % "Source device selection" +
708-
file_selection % "File to save to"
707+
file_selection % "File to save to" +
708+
device_selection % "Source device selection"
709709
)
710710
);
711711
}
@@ -862,7 +862,7 @@ struct link_command : public cmd {
862862
named_file_selection_x("infile1", 1) % "Files to link" +
863863
named_file_selection_x("infile2", 2) % "Files to link" +
864864
optional_file_selection_x("infile3", 3) % "Files to link" +
865-
option('p', "--pad") & hex("pad").set(settings.link.align) % "Specify alignment to pad to, defaults to 0x1000"
865+
(option('p', "--pad") & hex("pad").set(settings.link.align)) % "Specify alignment to pad to, defaults to 0x1000"
866866
);
867867
}
868868

@@ -965,12 +965,12 @@ struct otp_list_command : public cmd {
965965
"ROW_NAME to select a whole row by name.\n" \
966966
"ROW_NUMBER to select a whole row by number.\n" \
967967
"PAGE:PAGE_ROW_NUMBER to select a whole row by page and number within page.\n\n" \
968-
"... or can select a single field/subset of a row (where REG_SEL is one of the above row selectors):\n\n"
969-
"REG_SEL.FIELD_NAME to select a field within a row by name.\n" \
970-
"REG_SEL.n-m to select a range of bits within a row.\n" \
971-
"REG_SEL.n to select a single bit within a row.\n" \
968+
"... or can select a single field/subset of a row (where ROW_SEL is one of the above row selectors):\n\n"
969+
"ROW_SEL.FIELD_NAME to select a field within a row by name.\n" \
970+
"ROW_SEL.n-m to select a range of bits within a row.\n" \
971+
"ROW_SEL.n to select a single bit within a row.\n" \
972972
".FIELD_NAME to select any row's field by name.\n\n" \
973-
".. or can selected multiple rows by using blank or '*' for PAGE or PAGE_ROW_NUMBER").repeatable().min(0)
973+
".. or can select multiple rows by using blank or '*' for PAGE or PAGE_ROW_NUMBER").repeatable().min(0)
974974
) % "Row/Field Selection"
975975
);
976976
}
@@ -989,7 +989,7 @@ struct otp_get_command : public cmd {
989989
return (
990990
(
991991
(option('c', "--copies") & integer("copies").min(1).set(settings.otp.redundancy)) % "Read multiple redundant values" +
992-
option('r', "--raw").set(settings.otp.raw) % "Get raw 24 bit values" +
992+
option('r', "--raw").set(settings.otp.raw) % "Get raw 24-bit values" +
993993
option('e', "--ecc").set(settings.otp.ecc) % "Use error correction" +
994994
option('n', "--no-descriptions").set(settings.otp.list_no_descriptions) % "Don't show descriptions" +
995995
(option('i', "--include") & value("filename").add_to(settings.otp.extra_files)).min(0).max(1) % "Include extra otp definition" // todo more than 1
@@ -1004,12 +1004,12 @@ struct otp_get_command : public cmd {
10041004
"ROW_NAME to select a whole row by name.\n" \
10051005
"ROW_NUMBER to select a whole row by number.\n" \
10061006
"PAGE:PAGE_ROW_NUMBER to select a whole row by page and number within page.\n\n" \
1007-
"... or can select a single field/subset of a row (where REG_SEL is one of the above row selectors):\n\n"
1008-
"REG_SEL.FIELD_NAME to select a field within a row by name.\n" \
1009-
"REG_SEL.n-m to select a range of bits within a row.\n" \
1010-
"REG_SEL.n to select a single bit within a row.\n" \
1007+
"... or can select a single field/subset of a row (where ROW_SEL is one of the above row selectors):\n\n"
1008+
"ROW_SEL.FIELD_NAME to select a field within a row by name.\n" \
1009+
"ROW_SEL.n-m to select a range of bits within a row.\n" \
1010+
"ROW_SEL.n to select a single bit within a row.\n" \
10111011
".FIELD_NAME to select any row's field by name.\n\n" \
1012-
".. or can selected multiple rows by using blank or '*' for PAGE or PAGE_ROW_NUMBER").repeatable().min(0)
1012+
".. or can select multiple rows by using blank or '*' for PAGE or PAGE_ROW_NUMBER").repeatable().min(0)
10131013
) % "Row/Field Selection"
10141014
);
10151015
}
@@ -1028,7 +1028,7 @@ struct otp_dump_command : public cmd {
10281028
group get_cli() override {
10291029
return (
10301030
(
1031-
option('r', "--raw").set(settings.otp.raw) % "Get raw 24 bit values" +
1031+
option('r', "--raw").set(settings.otp.raw) % "Get raw 24-bit values. This is the default" +
10321032
option('e', "--ecc").set(settings.otp.ecc) % "Use error correction"
10331033
).min(0).doc_non_optional(true) % "Row/field options" +
10341034
(
@@ -1050,7 +1050,7 @@ struct otp_load_command : public cmd {
10501050
group get_cli() override {
10511051
return (
10521052
(
1053-
option('r', "--raw").set(settings.otp.raw) % "Get raw 24 bit values" +
1053+
option('r', "--raw").set(settings.otp.raw) % "Set raw 24-bit values. This is the default for BIN files" +
10541054
option('e', "--ecc").set(settings.otp.ecc) % "Use error correction" +
10551055
(option('s', "--start_row") & integer("row").set(settings.otp.row)) % "Start row to load at (note use 0x for hex)" +
10561056
(option('i', "--include") & value("filename").add_to(settings.otp.extra_files)).min(0).max(1) % "Include extra otp definition" // todo more than 1
@@ -1061,7 +1061,7 @@ struct otp_load_command : public cmd {
10611061
}
10621062

10631063
string get_doc() const override {
1064-
return "Load the row range stored in a file into OTP and verify. Data is 2 bytes/row for ECC, 4 bytes/row for raw.";
1064+
return "Load the row range stored in a file into OTP and verify. Data is 2 bytes/row for ECC, 4 bytes/row for raw (MSB is ignored).";
10651065
}
10661066
};
10671067

@@ -1074,19 +1074,24 @@ struct otp_set_command : public cmd {
10741074
group get_cli() override {
10751075
return (
10761076
(
1077-
(option('c', "--copies") & integer("copies").min(1).set(settings.otp.redundancy)) % "Read multiple redundant values" +
1078-
option('r', "--raw").set(settings.otp.raw) % "Set raw 24 bit values" +
1077+
(option('c', "--copies") & integer("copies").min(1).set(settings.otp.redundancy)) % "Write multiple redundant values" +
1078+
option('r', "--raw").set(settings.otp.raw) % "Set raw 24-bit values" +
10791079
option('e', "--ecc").set(settings.otp.ecc) % "Use error correction" +
10801080
option('s', "--set-bits").set(settings.otp.ignore_set) % "Set bits only" +
10811081
(option('i', "--include") & value("filename").add_to(settings.otp.extra_files)).min(0).max(1) % "Include extra otp definition" // todo more than 1
10821082
).min(0).doc_non_optional(true) % "Redundancy/Error Correction Overrides" +
10831083
(
10841084
option('z', "--fuzzy").set(settings.otp.fuzzy) % "Allow fuzzy name searches in selector vs exact match" +
10851085
(value("selector").add_to(settings.otp.selectors) %
1086-
"The row/field selector, which can be:\nROW_NAME or ROW_NUMBER or PAGE:PAGE_ROW_NUMBER to select a whole row.\n"
1087-
"FIELD, REG.FIELD, REG.n-m, PAGE:PAGE_ROW_NUMBER.FIELD or PAGE:PAGE_ROW_NUMBER.n-m to select a row field.\n\n"
1088-
"where:\n\nREG and FIELD are names (or parts of names with fuzzy searches).\nPAGE and PAGE_ROW_NUMBER are page numbers and row within a page, "
1089-
"ROW_NUMBER is an absolute row number offset, and n-m are the inclusive bit ranges of a field.")
1086+
"The row/field selector, which can select a whole row:\n\n" \
1087+
"ROW_NAME to select a whole row by name.\n" \
1088+
"ROW_NUMBER to select a whole row by number.\n" \
1089+
"PAGE:PAGE_ROW_NUMBER to select a whole row by page and number within page.\n\n" \
1090+
"... or can select a single field/subset of a row (where ROW_SEL is one of the above row selectors):\n\n"
1091+
"ROW_SEL.FIELD_NAME to select a field within a row by name.\n" \
1092+
"ROW_SEL.n-m to select a range of bits within a row.\n" \
1093+
"ROW_SEL.n to select a single bit within a row.\n" \
1094+
".FIELD_NAME to select any row's field by name.")
10901095
) % "Row/Field Selection" +
10911096
integer("value").set(settings.otp.value) % "The value to set" +
10921097
(
@@ -7882,7 +7887,10 @@ static void sleep_ms(int ms) {
78827887
}
78837888

78847889
void get_terminal_size(int& width, int& height) {
7885-
#if defined(_WIN32)
7890+
#if defined(DOCS_WIDTH)
7891+
width = DOCS_WIDTH;
7892+
height = 24;
7893+
#elif defined(_WIN32)
78867894
CONSOLE_SCREEN_BUFFER_INFO csbi;
78877895
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
78887896
width = (int)(csbi.dwSize.X);

0 commit comments

Comments
 (0)
Please sign in to comment.