blob: c402d2308e9d17ae26d5f6f2a359bb917436c0e8 [file] [log] [blame]
b.liue9582032025-04-17 19:18:16 +08001#!/bin/sh
2
3# Packages data
4#
5# Notes:
6# * python3-codecs: Also contains codecs for CJK encodings but we don't
7# have a good way to check for uses
8# * python3-openssl: Don't include hashlib as it supports several
9# standard algorithms without requiring OpenSSL
10
11packages="
12python3-asyncio: asyncio
13python3-cgi: cgi
14python3-cgitb: cgitb
15python3-codecs: unicodedata
16python3-ctypes: ctypes
17python3-dbm: dbm dbm.dumb dbm.gnu dbm.ndbm
18python3-decimal: decimal
19python3-distutils: distutils
20python3-email: email
21python3-logging: logging
22python3-lzma: lzma
23python3-multiprocessing: multiprocessing
24python3-ncurses: ncurses
25python3-openssl: ssl
26python3-pydoc: doctest pydoc
27python3-readline: readline
28python3-sqlite3: sqlite3
29python3-unittest: unittest
30python3-urllib: urllib
31python3-uuid: uuid
32python3-xml: xml xmlrpc
33"
34
35
36# Constants
37
38stdin_name="<stdin>"
39grep_dir_filters="
40-Ir
41--include=*.py
42--exclude=setup.py
43--exclude=test_*.py
44--exclude=*_test.py
45--exclude-dir=test
46--exclude-dir=tests
47--exclude-dir=ipkg-*
48--exclude-dir=.pkgdir
49"
50
51log_level_notice=5
52log_level_info=6
53log_level_debug=7
54
55# /usr/include/sysexits.h
56ex_usage=64
57ex_noinput=66
58ex_unavailable=69
59ex_software=70
60
61newline="
62"
63oifs="$IFS"
64
65
66# Defaults
67
68grep_output_default_max_count=3
69grep_output_default_color_when="auto"
70grep_output_default_line_prefix="-HnT --label=$stdin_name"
71grep_output_default_context_num=1
72
73log_level_default="$log_level_info"
74
75
76# Global variables
77
78log_level=
79grep=
80grep_output_options=
81grep_output_description=
82stdin=
83output_name=
84is_first_search=
85
86
87# Logging
88
89log() {
90 printf '%s\n' "$*"
91}
92
93can_log_notice() {
94 [ "$log_level" -ge "$log_level_notice" ]
95}
96
97can_log_info() {
98 [ "$log_level" -ge "$log_level_info" ]
99}
100
101can_log_debug() {
102 [ "$log_level" -ge "$log_level_debug" ]
103}
104
105log_notice() {
106 if can_log_notice; then
107 log "$@"
108 fi
109}
110
111log_info() {
112 if can_log_info; then
113 log "$@"
114 fi
115}
116
117log_debug() {
118 if can_log_debug; then
119 log "$@"
120 fi
121}
122
123log_error() {
124 printf '%s\n' "Error: $*" >&2
125}
126
127
128# Usage
129
130usage() {
131 cat <<- EOF
132 Usage: ${0##*/} [OPTION]... [FILE]...
133 Search for imports of certain Python standard libraries in each FILE,
134 generate a list of suggested package dependencies.
135
136 With no FILE, or when FILE is -, read standard input.
137
138 Options:
139 -c WHEN use color in output;
140 WHEN is 'always', 'never', or 'auto' (default: '$grep_output_default_color_when')
141 -h display this help text and exit
142 -m NUM show max NUM matches per package per file (default: $grep_output_default_max_count);
143 use 0 to show all matches
144 -n NAME when one or no FILE is given, use NAME instead of FILE in
145 displayed information
146 -q show suggested dependencies only
147 -v show verbose output (also show all matches)
148 -x NUM show NUM lines of context (default: $grep_output_default_context_num)
149
150 EOF
151}
152
153
154# Imports search
155
156get_package_modules() {
157 local line="$1"
158 local field_num=0
159 local IFS=:
160
161 for field in $line; do
162 # strip leading and trailing whitespace
163 field="${field#"${field%%[! ]*}"}"
164 field="${field%"${field##*[! ]}"}"
165
166 # set variables in search_path()
167 if [ "$field_num" -eq 0 ]; then
168 package="$field"
169 field_num=1
170 elif [ "$field_num" -eq 1 ]; then
171 modules="$field"
172 field_num=2
173 else
174 field_num=3
175 fi
176 done
177
178 if [ "$field_num" -ne 2 ] || [ -z "$package" ] || [ -z "$modules" ]; then
179 log_error "invalid package data \"$line\""
180 exit "$ex_software"
181 fi
182}
183
184search_path_for_modules() {
185 local path="$1"
186 local path_is_dir="$2"
187 local path_is_stdin="$3"
188 local package="$4"
189 local modules="$5"
190 local modules_regex
191 local regex
192 local remove_dir_prefix
193 local grep_results
194 local IFS="$oifs"
195
196 log_debug " Looking for modules in $package ($modules)"
197
198 modules_regex=$(printf '%s' "$modules" | sed -e 's/\./\\./g' -e 's/\s\+/|/g')
199 regex="\b(import\s+($modules_regex)|from\s+($modules_regex)\S*\s+import)\b"
200
201 if [ -n "$path_is_dir" ]; then
202 remove_dir_prefix="s|^\(\(\x1b[[0-9;]*[mK]\)*\)$path|\1|"
203 grep_results=$($grep $grep_output_options $grep_dir_filters -E "$regex" "$path")
204
205 elif [ -n "$path_is_stdin" ]; then
206 grep_results=$(printf '%s\n' "$stdin" | $grep $grep_output_options -E "$regex")
207
208 else
209 grep_results=$($grep $grep_output_options -E "$regex" "$path")
210 fi
211
212 if [ "$?" -ne 0 ]; then
213 log_debug " No imports found"
214 log_debug ""
215 return 0
216 fi
217
218 log_info " Found imports for: $modules"
219
220 if can_log_info; then
221 printf '%s\n' "$grep_results" | sed -e "$remove_dir_prefix" -e "s/^/ /"
222 fi
223
224 log_info ""
225
226 # set variable in search_path()
227 suggested="$suggested +$package"
228}
229
230search_path() {
231 local path="$1"
232 local name="$2"
233 local path_is_dir
234 local path_is_stdin
235 local package
236 local modules
237 local suggested
238 local IFS="$newline"
239
240 if [ "$path" = "-" ]; then
241 path_is_stdin=1
242
243 else
244 if ! [ -e "$path" ]; then
245 log_error "$path does not exist"
246 exit "$ex_noinput"
247 fi
248
249 if [ -d "$path" ]; then
250 path="${path%/}/"
251 path_is_dir=1
252 fi
253 fi
254
255 log_info ""
256 log_info "Searching $name (showing $grep_output_description):"
257 log_info ""
258
259 if [ -n "$path_is_stdin" ]; then
260 stdin="$(cat)"
261 fi
262
263 for line in $packages; do
264 # strip leading whitespace
265 line="${line#"${line%%[! ]*}"}"
266
267 # skip blank lines or comments (beginning with #)
268 if [ -z "$line" ] || [ "$line" != "${line###}" ]; then
269 continue
270 fi
271
272 package=
273 modules=
274
275 get_package_modules "$line"
276 search_path_for_modules "$path" "$path_is_dir" "$path_is_stdin" "$package" "$modules"
277 done
278
279 log_notice "Suggested dependencies for $name:"
280
281 if [ -z "$suggested" ]; then
282 suggested="(none)"
283 fi
284 IFS="$oifs"
285 for package in $suggested; do
286 log_notice " $package"
287 done
288
289 log_notice ""
290}
291
292
293# Find GNU grep
294
295if ggrep --version 2>&1 | grep -q GNU; then
296 grep="ggrep"
297elif grep --version 2>&1 | grep -q GNU; then
298 grep="grep"
299else
300 log_error "cannot find GNU grep"
301 exit "$ex_unavailable"
302fi
303
304
305# Process environment variables
306
307case $PYTHON3_FIND_STDLIB_DEPENDS_LOG_LEVEL in
308 notice)
309 log_level="$log_level_notice"
310 ;;
311 info)
312 log_level="$log_level_info"
313 ;;
314 debug)
315 log_level="$log_level_debug"
316 ;;
317 *)
318 log_level="$log_level_default"
319 ;;
320esac
321
322grep_output_max_count="${PYTHON3_FIND_STDLIB_DEPENDS_MAX_COUNT:-$grep_output_default_max_count}"
323grep_output_color_when="${PYTHON3_FIND_STDLIB_DEPENDS_COLOR_WHEN:-$grep_output_default_color_when}"
324grep_output_line_prefix="${PYTHON3_FIND_STDLIB_DEPENDS_LINE_PREFIX:-$grep_output_default_line_prefix}"
325grep_output_context_num="${PYTHON3_FIND_STDLIB_DEPENDS_CONTEXT_NUM:-$grep_output_default_context_num}"
326
327
328# Process command line options
329
330while getopts c:hm:n:qvx: OPT; do
331 case $OPT in
332 c)
333 grep_output_color_when="$OPTARG"
334 ;;
335 h)
336 usage
337 exit 0
338 ;;
339 m)
340 grep_output_max_count="$OPTARG"
341 ;;
342 n)
343 output_name="$OPTARG"
344 ;;
345 q)
346 log_level="$log_level_notice"
347 ;;
348 v)
349 log_level="$log_level_debug"
350 ;;
351 x)
352 grep_output_context_num="$OPTARG"
353 ;;
354 \?)
355 usage
356 exit "$ex_usage"
357 ;;
358 esac
359done
360
361shift $((OPTIND - 1))
362
363
364# Set up grep output options
365
366if can_log_info; then
367 if [ "$grep_output_color_when" = "auto" ]; then
368 if [ -t 1 ]; then
369 grep_output_color_when="always"
370 else
371 grep_output_color_when="never"
372 fi
373 fi
374
375 if ! can_log_debug && [ "$grep_output_max_count" -gt 0 ]; then
376 grep_output_options="-m $grep_output_max_count"
377
378 if [ "$grep_output_max_count" -eq 1 ]; then
379 grep_output_description="max 1 match per file"
380 else
381 grep_output_description="max $grep_output_max_count matches per file"
382 fi
383
384 else
385 grep_output_description="all matches"
386 fi
387
388 if [ "$grep_output_context_num" -gt 0 ]; then
389 grep_output_options="$grep_output_options -C $grep_output_context_num"
390
391 if [ "$grep_output_context_num" -eq 1 ]; then
392 grep_output_description="$grep_output_description, 1 line of context"
393 else
394 grep_output_description="$grep_output_description, $grep_output_context_num lines of context"
395 fi
396 fi
397
398 grep_output_options="$grep_output_options --color=$grep_output_color_when $grep_output_line_prefix"
399
400else
401 grep_output_options="-q"
402fi
403
404
405# Main
406
407if [ "$#" -gt 0 ]; then
408 is_first_search=1
409
410 if [ "$#" -gt 1 ]; then
411 output_name=
412 fi
413
414 for path; do
415 if [ -z "$is_first_search" ]; then
416 log_info "===="
417 fi
418
419 if [ -z "$output_name" ]; then
420 if [ "$path" = "-" ]; then
421 output_name="$stdin_name"
422 else
423 output_name="$path"
424 fi
425 fi
426
427 search_path "$path" "$output_name"
428
429 is_first_search=
430 output_name=
431 done
432
433else
434 search_path "-" "${output_name:-$stdin_name}"
435fi