threads / patch / 63296

patch, 4 partsDrop Perl dependency in a couple of subsystems

Subject: [PATCH 0/4] Drop Perl dependency in a couple of subsystems

## tl;dr

14 messages between Apr 15, 2025 and Apr 16, 2025. Diffs are folded; open one to read it.

replies: 13people: 2as markdown or json

Patrick Steinhardt· Apr 15, 2025, 09:57 UTC · lore
Hi,

this patch series is a follow-up to my recent series that got rid of our strict dependency on Perl for running our test suite [1]. The goal of it is to remove the dependency on Perl in a couple more areas:

  - git-filter-branch(1)
  - git-request-pull(1)
  - Building our documentation.

With these converted you can get a fully-functioning Git distribution with only a small handful of features missing:

  - gitweb.
  - Scripts to import repositories from GNU Arch, CVS and Subversion.
  - git-send-email.
  - Our Perl bindings, obviously.
  - The netrc credential helper from "contrib/".

None of these feel critical for day-to-day usage of Git, so Perl is now a truly-optional dependency of Git.

The series is built on top of 485f5f86361 (The fourth batch, 2025-04-08) with ps/test-wo-perl-prereq at 7a7b6022670 (t5703: refactor test to not depend on Perl, 2025-04-03) merged into it.

Thanks!
Patrick
[1]: <20250320-b4-pks-t-perlless-v1-0-b1eefe27ac55@pks.im>
---
Patrick Steinhardt (4):
      filter-branch: stop depending on Perl
      request-pull: stop depending on Perl
      Documentation: stop depending on Perl to massage user manual
      Documentation: stop depending on Perl to generate command list
 Documentation/Makefile      |   8 ++--
 Documentation/cmd-list.perl |  80 ----------------------------------
 Documentation/cmd-list.sh   | 104 ++++++++++++++++++++++++++++++++++++++++++++
 Documentation/fix-texi.perl |  15 -------
 Documentation/fix-texi.sh   |  21 +++++++++
 Documentation/meson.build   |   4 +-
 git-filter-branch.sh        |  37 ++++++++--------
 git-request-pull.sh         |  74 ++++++++++++++++---------------
 meson.build                 |   2 +-
 t/t5150-request-pull.sh     |   6 ---
 10 files changed, 191 insertions(+), 160 deletions(-)

--- base-commit: b09b3d4791ed248fed65de18cd6d8c084f7f0280 change-id: 20250409-b4-pks-drop-perl-574571234e3c

Patrick Steinhardt· Apr 15, 2025, 09:57 UTC · re: Patrick Steinhardt · lore

[PATCH 1/4] filter-branch: stop depending on Perl

While git-filter-branch(1) is written as a shell script, the `--state-branch` feature depends on Perl to save and extract the object ID mappings. This can lead to subtle breakage though:

  - We execute `perl` directly without respecting the `PERL_PATH`
    configured by the distribution. As such, it may happen that we use
    the wrong version of Perl.
  - We install the script unchanged even if Perl isn't available at all
    on the system, so using `--state-branch` would lead to failure
    altogether in that case.

Fix this by dropping Perl and instead implementing the feature with shell scripting exclusively.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 git-filter-branch.sh | 37 +++++++++++++++++++------------------
 1 file changed, 19 insertions(+), 18 deletions(-)
Show changes to git-filter-branch.sh +19 −18
diff --git a/git-filter-branch.sh b/git-filter-branch.sh
index 3a51d4507c7..24fa317aaaa 100755
--- a/git-filter-branch.sh
+++ b/git-filter-branch.sh
@@ -295,15 +295,18 @@ then
 	if test -n "$state_commit"
 	then
 		echo "Populating map from $state_branch ($state_commit)" 1>&2
-		perl -e'open(MAP, "-|", "git show $ARGV[0]:filter.map") or die;
-			while (<MAP>) {
-				m/(.*):(.*)/ or die;
-				open F, ">../map/$1" or die;
-				print F "$2" or die;
-				close(F) or die;
-			}
-			close(MAP) or die;' "$state_commit" \
-				|| die "Unable to load state from $state_branch:filter.map"
+
+		git show "$state_commit:filter.map" >"$tempdir"/filter-map ||
+			die "Unable to load state from $state_branch:filter.map"
+		while read line
+		do
+			case "$line" in
+			*:*)
+				echo "${line%:*}" >../map/"${line#*:}";;
+			*)
+				die "Unable to load state from $state_branch:filter.map";;
+			esac
+		done <"$tempdir"/filter-map
 	else
 		echo "Branch $state_branch does not exist. Will create" 1>&2
 	fi
@@ -633,15 +636,13 @@ if test -n "$state_branch"
 then
 	echo "Saving rewrite state to $state_branch" 1>&2
 	state_blob=$(
-		perl -e'opendir D, "../map" or die;
-			open H, "|-", "git hash-object -w --stdin" or die;
-			foreach (sort readdir(D)) {
-				next if m/^\.\.?$/;
-				open F, "<../map/$_" or die;
-				chomp($f = <F>);
-				print H "$_:$f\n" or die;
-			}
-			close(H) or die;' || die "Unable to save state")
+		for file in ../map/*
+		do
+			from_commit=$(basename "$file")
+			to_commit=$(cat "$file")
+			echo "$from_commit:$to_commit"
+		done | git hash-object -w --stdin || die "Unable to save state"
+	)
 	state_tree=$(printf '100644 blob %s\tfilter.map\n' "$state_blob" | git mktree)
 	if test -n "$state_commit"
 	then
-- 
2.49.0.805.g082f7c87e0.dirty
Junio C Hamano· Apr 15, 2025, 15:50 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 1/4] filter-branch: stop depending on Perl

Patrick Steinhardt <ps@pks.im> writes:
Show 14 quoted lines
> While git-filter-branch(1) is written as a shell script, the
> `--state-branch` feature depends on Perl to save and extract the object
> ID mappings. This can lead to subtle breakage though:
>
>   - We execute `perl` directly without respecting the `PERL_PATH`
>     configured by the distribution. As such, it may happen that we use
>     the wrong version of Perl.
>
>   - We install the script unchanged even if Perl isn't available at all
>     on the system, so using `--state-branch` would lead to failure
>     altogether in that case.
>
> Fix this by dropping Perl and instead implementing the feature with
> shell scripting exclusively.
Excellent.

I just realized that the first bullet point above is a good argument against "#!/usr/bin/env perl" as well. If we were to go the "#!/usr/bin/env perl" direction, we should stop pretending with $PERL_PATH that we'd consistently use the same version of Perl chosen at build-install time, and we just trust users' $PATH, for consistency.

Show 34 quoted lines
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  git-filter-branch.sh | 37 +++++++++++++++++++------------------
>  1 file changed, 19 insertions(+), 18 deletions(-)
>
> diff --git a/git-filter-branch.sh b/git-filter-branch.sh
> index 3a51d4507c7..24fa317aaaa 100755
> --- a/git-filter-branch.sh
> +++ b/git-filter-branch.sh
> @@ -295,15 +295,18 @@ then
>  	if test -n "$state_commit"
>  	then
>  		echo "Populating map from $state_branch ($state_commit)" 1>&2
> -		perl -e'open(MAP, "-|", "git show $ARGV[0]:filter.map") or die;
> -			while (<MAP>) {
> -				m/(.*):(.*)/ or die;
> -				open F, ">../map/$1" or die;
> -				print F "$2" or die;
> -				close(F) or die;
> -			}
> -			close(MAP) or die;' "$state_commit" \
> -				|| die "Unable to load state from $state_branch:filter.map"
> +
> +		git show "$state_commit:filter.map" >"$tempdir"/filter-map ||
> +			die "Unable to load state from $state_branch:filter.map"
> +		while read line
> +		do
> +			case "$line" in
> +			*:*)
> +				echo "${line%:*}" >../map/"${line#*:}";;
> +			*)
> +				die "Unable to load state from $state_branch:filter.map";;
> +			esac
> +		done <"$tempdir"/filter-map

Quite straight-forward. Do problematic bytes like backslashes appear in the blob that Perl m/(.*):(.*)/ would have passed intact without problems but may cause problems to the reimplementation? I doubt it.

Not in a scope of this change, but use of "git show" in place of "git cat-file blob" makes my skin somewhat tingle.

Show 17 quoted lines
>  	else
>  		echo "Branch $state_branch does not exist. Will create" 1>&2
>  	fi
> @@ -633,15 +636,13 @@ if test -n "$state_branch"
>  then
>  	echo "Saving rewrite state to $state_branch" 1>&2
>  	state_blob=$(
> -		perl -e'opendir D, "../map" or die;
> -			open H, "|-", "git hash-object -w --stdin" or die;
> -			foreach (sort readdir(D)) {
> -				next if m/^\.\.?$/;
> -				open F, "<../map/$_" or die;
> -				chomp($f = <F>);
> -				print H "$_:$f\n" or die;
> -			}
> -			close(H) or die;' || die "Unable to save state")
> +		for file in ../map/*

OK. * won't match . or .. so we no longer need to filter. Like the original, if we had any random cruft file, we may copy such garbage to the output, but that is not a new problem at all.

Nicely done.
Show 9 quoted lines
> +		do
> +			from_commit=$(basename "$file")
> +			to_commit=$(cat "$file")
> +			echo "$from_commit:$to_commit"
> +		done | git hash-object -w --stdin || die "Unable to save state"
> +	)
>  	state_tree=$(printf '100644 blob %s\tfilter.map\n' "$state_blob" | git mktree)
>  	if test -n "$state_commit"
>  	then
Patrick Steinhardt· Apr 15, 2025, 09:57 UTC · re: Patrick Steinhardt · lore

[PATCH 2/4] request-pull: stop depending on Perl

While git-request-pull(1) is written as a shell script, for it to function we depend on Perl being available. The script gets installed unconditionally though, regardless of whether or not Perl is even available on the system. When it's not available, the `@PERL_PATH@` variable may be substituted with a nonexistent executable path and thus cause the script to fail.

Refactor the script so that it does not depend on Perl at all anymore.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 git-request-pull.sh     | 74 ++++++++++++++++++++++++++-----------------------
 t/t5150-request-pull.sh |  6 ----
 2 files changed, 40 insertions(+), 40 deletions(-)
Show changes to 2 files +40 −40

git-request-pull.sh, t/t5150-request-pull.sh

diff --git a/git-request-pull.sh b/git-request-pull.sh
index 775ba8ea11a..59276fe265b 100755
--- a/git-request-pull.sh
+++ b/git-request-pull.sh
@@ -78,41 +78,47 @@ fi
 merge_base=$(git merge-base $baserev $headrev) ||
 die "fatal: No commits in common between $base and $head"
 
-# $head is the refname from the command line.
-# Find a ref with the same name as $head that exists at the remote
+find_matching_ref () {
+	while read sha1 ref
+	do
+		case "$ref" in
+		*"^{}")
+			ref="${ref%"^{}"}"
+			deref=true
+			;;
+		*)
+			deref=
+			;;
+		esac
+
+		if test "$sha1" = "${remote:-HEAD}"
+		then
+			echo "$sha1 $sha1"
+			break
+		fi
+
+		case "$ref" in
+		"${remote:-HEAD}"|*"/${remote:-HEAD}")
+			if test -z "$deref"
+			then
+				# Remember the matching unpeeled object on the
+				# remote side.
+				remote_sha1="$sha1"
+			fi
+
+			if test "$sha1" = "$headrev"
+			then
+				echo "${remote_sha1:-$headrev} $ref"
+				break
+			fi
+			;;
+		esac
+	done
+}
+
+# Find a ref with the same name as $remote that exists at the remote
 # and points to the same commit as the local object.
-find_matching_ref='
-	my ($head,$headrev) = (@ARGV);
-	my $pattern = qr{/\Q$head\E$};
-	my ($remote_sha1, $found);
-
-	while (<STDIN>) {
-		chomp;
-		my ($sha1, $ref, $deref) = /^(\S+)\s+([^^]+)(\S*)$/;
-
-		if ($sha1 eq $head) {
-			$found = $remote_sha1 = $sha1;
-			break;
-		}
-
-		if ($ref eq $head || $ref =~ $pattern) {
-			if ($deref eq "") {
-				# Remember the matching object on the remote side
-				$remote_sha1 = $sha1;
-			}
-			if ($sha1 eq $headrev) {
-				$found = $ref;
-				break;
-			}
-		}
-	}
-	if ($found) {
-		$remote_sha1 = $headrev if ! defined $remote_sha1;
-		print "$remote_sha1 $found\n";
-	}
-'
-
-set fnord $(git ls-remote "$url" | @PERL_PATH@ -e "$find_matching_ref" "${remote:-HEAD}" "$headrev")
+set fnord $(git ls-remote "$url" | find_matching_ref)
 remote_sha1=$2
 ref=$3
 
diff --git a/t/t5150-request-pull.sh b/t/t5150-request-pull.sh
index cb67bac1c47..270ce6ea487 100755
--- a/t/t5150-request-pull.sh
+++ b/t/t5150-request-pull.sh
@@ -7,12 +7,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
 . ./test-lib.sh
 
-if ! test_have_prereq PERL
-then
-	skip_all='skipping request-pull tests, perl not available'
-	test_done
-fi
-
 test_expect_success 'setup' '
 
 	git init --bare upstream.git &&
-- 
2.49.0.805.g082f7c87e0.dirty
Junio C Hamano· Apr 15, 2025, 16:16 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 2/4] request-pull: stop depending on Perl

Patrick Steinhardt <ps@pks.im> writes:
Show 32 quoted lines
> While git-request-pull(1) is written as a shell script, for it to
> function we depend on Perl being available. The script gets installed
> unconditionally though, regardless of whether or not Perl is even
> available on the system. When it's not available, the `@PERL_PATH@`
> variable may be substituted with a nonexistent executable path and thus
> cause the script to fail.
>
> Refactor the script so that it does not depend on Perl at all anymore.
>
> Signed-off-by: Patrick Steinhardt <ps@pks.im>
> ---
>  git-request-pull.sh     | 74 ++++++++++++++++++++++++++-----------------------
>  t/t5150-request-pull.sh |  6 ----
>  2 files changed, 40 insertions(+), 40 deletions(-)
>
> diff --git a/git-request-pull.sh b/git-request-pull.sh
> index 775ba8ea11a..59276fe265b 100755
> --- a/git-request-pull.sh
> +++ b/git-request-pull.sh
> @@ -78,41 +78,47 @@ fi
>  merge_base=$(git merge-base $baserev $headrev) ||
>  die "fatal: No commits in common between $base and $head"
>  
> -# $head is the refname from the command line.
> -# Find a ref with the same name as $head that exists at the remote
> +find_matching_ref () {
> +	while read sha1 ref
> +	do
> +		case "$ref" in
> +		*"^{}")
> +			ref="${ref%"^{}"}"
> +			deref=true

This got a bit tighter (the original allowed ls-remote output to be later extended to throw "^something-else" at the end of line), which I do not know is something we need to worry about. I think retaining the original semantics is easy in this case, e.g.,

		*"^"?*)
			ref="${ref%"^*"}"

or something, if we wanted to. As this is meant to be faithful rewrite to lose Perl, not a bugfix to correct overly loose pattern matching in the original, I think we want to in this case.

Show 38 quoted lines
> +			;;
> +		*)
> +			deref=
> +			;;
> +		esac
> +
> +		if test "$sha1" = "${remote:-HEAD}"
> +		then
> +			echo "$sha1 $sha1"
> +			break
> +		fi
> +
> +		case "$ref" in
> +		"${remote:-HEAD}"|*"/${remote:-HEAD}")
> +			if test -z "$deref"
> +			then
> +				# Remember the matching unpeeled object on the
> +				# remote side.
> +				remote_sha1="$sha1"
> +			fi
> +
> +			if test "$sha1" = "$headrev"
> +			then
> +				echo "${remote_sha1:-$headrev} $ref"
> +				break
> +			fi
> +			;;
> +		esac
> +	done
> +}
> +
> +# Find a ref with the same name as $remote that exists at the remote
>  # and points to the same commit as the local object.
> ...
> -set fnord $(git ls-remote "$url" | @PERL_PATH@ -e "$find_matching_ref" "${remote:-HEAD}" "$headrev")
> +set fnord $(git ls-remote "$url" | find_matching_ref)
>  remote_sha1=$2
>  ref=$3

This is faithful to the original, and having find_matching_ref in the process substitution means we do not have to worry about localizing variables in there. Good.

Patrick Steinhardt· Apr 16, 2025, 15:07 UTC · re: Junio C Hamano · lore

Re: [PATCH 2/4] request-pull: stop depending on Perl

On Tue, Apr 15, 2025 at 09:16:05AM -0700, Junio C Hamano wrote:
Show 26 quoted lines
> Patrick Steinhardt <ps@pks.im> writes:
> > diff --git a/git-request-pull.sh b/git-request-pull.sh
> > index 775ba8ea11a..59276fe265b 100755
> > --- a/git-request-pull.sh
> > +++ b/git-request-pull.sh
> > @@ -78,41 +78,47 @@ fi
> >  merge_base=$(git merge-base $baserev $headrev) ||
> >  die "fatal: No commits in common between $base and $head"
> >  
> > -# $head is the refname from the command line.
> > -# Find a ref with the same name as $head that exists at the remote
> > +find_matching_ref () {
> > +	while read sha1 ref
> > +	do
> > +		case "$ref" in
> > +		*"^{}")
> > +			ref="${ref%"^{}"}"
> > +			deref=true
> 
> This got a bit tighter (the original allowed ls-remote output to be
> later extended to throw "^something-else" at the end of line), which
> I do not know is something we need to worry about.  I think retaining
> the original semantics is easy in this case, e.g.,
> 
> 		*"^"?*)
> 			ref="${ref%"^*"}"

This should probably read `ref="${ref%"^"*}"`, with the `*` outside of the quotes.

> or something, if we wanted to.  As this is meant to be faithful
> rewrite to lose Perl, not a bugfix to correct overly loose pattern
> matching in the original, I think we want to in this case.

Fair enough. It should've been at least mentioned in the commit message, but let's instead just retain the current behaviour as suggested.

Patrick
Patrick Steinhardt· Apr 15, 2025, 09:57 UTC · re: Patrick Steinhardt · lore

[PATCH 3/4] Documentation: stop depending on Perl to massage user manual

The "fix-texi.perl" script is used to fix up the output of `docbook2x-texi`:

  - It changes the filename to be "git.info".
  - It changes the directory category and entry.

The script is written in Perl, but it can be rather trivially converted to a shell script. Do so to remove the dependency on Perl for building the user manual.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/Makefile      |  4 ++--
 Documentation/fix-texi.perl | 15 ---------------
 Documentation/fix-texi.sh   | 21 +++++++++++++++++++++
 3 files changed, 23 insertions(+), 17 deletions(-)
Show changes to 3 files +23 −17

Documentation/Makefile, Documentation/fix-texi.perl, Documentation/fix-texi.sh

diff --git a/Documentation/Makefile b/Documentation/Makefile
index 0d3a2c6bfe6..6485d40f620 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -398,9 +398,9 @@ user-manual.html: user-manual.xml $(XSLT)
 git.info: user-manual.texi
 	$(QUIET_MAKEINFO)$(MAKEINFO) --no-split -o $@ user-manual.texi
 
-user-manual.texi: user-manual.xml
+user-manual.texi: user-manual.xml fix-texi.sh
 	$(QUIET_DB2TEXI)$(DOCBOOK2X_TEXI) user-manual.xml --encoding=UTF-8 --to-stdout >$@+ && \
-	$(PERL_PATH) fix-texi.perl <$@+ >$@ && \
+	$(SHELL_PATH) fix-texi.sh <$@+ >$@ && \
 	$(RM) $@+
 
 user-manual.pdf: user-manual.xml
diff --git a/Documentation/fix-texi.perl b/Documentation/fix-texi.perl
deleted file mode 100755
index ff7d78f620a..00000000000
--- a/Documentation/fix-texi.perl
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/perl -w
-
-while (<>) {
-	if (/^\@setfilename/) {
-		$_ = "\@setfilename git.info\n";
-	} elsif (/^\@direntry/) {
-		print '@dircategory Development
-@direntry
-* Git: (git).           A fast distributed revision control system
-@end direntry
-';	}
-	unless (/^\@direntry/../^\@end direntry/) {
-		print;
-	}
-}
diff --git a/Documentation/fix-texi.sh b/Documentation/fix-texi.sh
new file mode 100755
index 00000000000..bc300f7b0f1
--- /dev/null
+++ b/Documentation/fix-texi.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+awk '
+	/^@setfilename/{
+		print "@setfilename git.info"
+		next
+	}
+	/^@direntry/{
+		direntry=1
+		print "@dircategory Development"
+		print "@direntry"
+		print "* Git: (git).           A fast distributed revision control system"
+		print "@end direntry"
+		next
+	}
+	/^@end direntry/{
+		direntry=0
+		next
+	}
+	!direntry
+'
-- 
2.49.0.805.g082f7c87e0.dirty
Patrick Steinhardt· Apr 15, 2025, 09:57 UTC · re: Patrick Steinhardt · lore

[PATCH 4/4] Documentation: stop depending on Perl to generate command list

The "cmd-list.perl" script is used to extract the list of commands part of a specific category and extracts the description of each command from its respective manpage. The generated output is then included in git(1) to list all Git commands.

The script is written in Perl. Refactor it to use shell scripting exclusively so that we can get rid of the mandatory dependency on Perl to build our documentation.

The converted script is slower compared to its Perl implementation. But by being careful and not spawning external commands in `format_one ()` we can mitigate the performance hit to a reasonable level:

    Benchmark 1: Perl
      Time (mean ± σ):      10.3 ms ±   0.2 ms    [User: 7.0 ms, System: 3.3 ms]
      Range (min … max):    10.0 ms …  11.1 ms    200 runs
    Benchmark 2: Shell
      Time (mean ± σ):      74.4 ms ±   0.4 ms    [User: 48.6 ms, System: 24.7 ms]
      Range (min … max):    73.1 ms …  75.5 ms    200 runs
    Summary
      Perl ran
        7.23 ± 0.13 times faster than Shell

While a sevenfold slowdown is significant, the benefit of not requiring Perl for a fully-functioning Git installation outweighs waiting a couple of milliseconds longer during the build process.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/Makefile      |   4 +-
 Documentation/cmd-list.perl |  80 ----------------------------------
 Documentation/cmd-list.sh   | 104 ++++++++++++++++++++++++++++++++++++++++++++
 Documentation/meson.build   |   4 +-
 meson.build                 |   2 +-
 5 files changed, 109 insertions(+), 85 deletions(-)
Show changes to 5 files +109 −85

Documentation/Makefile, Documentation/cmd-list.perl, Documentation/cmd-list.sh, Documentation/meson.build, meson.build

diff --git a/Documentation/Makefile b/Documentation/Makefile
index 6485d40f620..b109d25e9c8 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -317,8 +317,8 @@ cmds_txt = cmds-ancillaryinterrogators.adoc \
 
 $(cmds_txt): cmd-list.made
 
-cmd-list.made: cmd-list.perl ../command-list.txt $(MAN1_TXT)
-	$(QUIET_GEN)$(PERL_PATH) ./cmd-list.perl .. . $(cmds_txt) && \
+cmd-list.made: cmd-list.sh ../command-list.txt $(MAN1_TXT)
+	$(QUIET_GEN)$(SHELL_PATH) ./cmd-list.sh .. . $(cmds_txt) && \
 	date >$@
 
 mergetools-%.adoc: generate-mergetool-list.sh ../git-mergetool--lib.sh $(wildcard ../mergetools/*)
diff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl
deleted file mode 100755
index 0a0c1b3f611..00000000000
--- a/Documentation/cmd-list.perl
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/perl -w
-
-use File::Compare qw(compare);
-
-sub format_one {
-	my ($source_dir, $out, $nameattr) = @_;
-	my ($name, $attr) = @$nameattr;
-	my ($path) = "$source_dir/Documentation/$name.adoc";
-	my ($state, $description);
-	my $mansection;
-	$state = 0;
-	open I, '<', "$path" or die "No such file $path.adoc";
-	while (<I>) {
-		if (/^(?:git|scalar)[a-z0-9-]*\(([0-9])\)$/) {
-			$mansection = $1;
-			next;
-		}
-		if (/^NAME$/) {
-			$state = 1;
-			next;
-		}
-		if ($state == 1 && /^----$/) {
-			$state = 2;
-			next;
-		}
-		next if ($state != 2);
-		chomp;
-		$description = $_;
-		last;
-	}
-	close I;
-	if (!defined $description) {
-		die "No description found in $path.adoc";
-	}
-	if (my ($verify_name, $text) = ($description =~ /^($name) - (.*)/)) {
-		print $out "linkgit:$name\[$mansection\]::\n\t";
-		if ($attr =~ / deprecated /) {
-			print $out "(deprecated) ";
-		}
-		print $out "$text.\n\n";
-	}
-	else {
-		die "Description does not match $name: $description";
-	}
-}
-
-my ($source_dir, $build_dir, @categories) = @ARGV;
-
-open IN, "<$source_dir/command-list.txt";
-while (<IN>) {
-	last if /^### command list/;
-}
-
-my %cmds = ();
-for (sort <IN>) {
-	next if /^#/;
-
-	chomp;
-	my ($name, $cat, $attr) = /^(\S+)\s+(.*?)(?:\s+(.*))?$/;
-	$attr = '' unless defined $attr;
-	push @{$cmds{$cat}}, [$name, " $attr "];
-}
-close IN;
-
-for my $out (@categories) {
-	my ($cat) = $out =~ /^cmds-(.*)\.adoc$/;
-	my ($path) = "$build_dir/$out";
-	open O, '>', "$path+" or die "Cannot open output file $out+";
-	for (@{$cmds{$cat}}) {
-		format_one($source_dir, \*O, $_);
-	}
-	close O;
-
-	if (-f "$path" && compare("$path", "$path+") == 0) {
-		unlink "$path+";
-	}
-	else {
-		rename "$path+", "$path";
-	}
-}
diff --git a/Documentation/cmd-list.sh b/Documentation/cmd-list.sh
new file mode 100755
index 00000000000..fa90781f3c7
--- /dev/null
+++ b/Documentation/cmd-list.sh
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+set -e
+
+format_one () {
+	source_dir="$1"
+	command="$2"
+	attributes="$3"
+
+	path="$source_dir/Documentation/$command.adoc"
+	if ! test -f "$path"
+	then
+		echo >&2 "No such file $path"
+		exit 1
+	fi
+
+	state=0
+	while read line
+	do
+		case "$state" in
+			0)
+				case "$line" in
+				git*\(*\)|scalar*\(*\))
+					mansection="${line##*\(}"
+					mansection="${mansection%\)}"
+					;;
+				NAME)
+					state=1;;
+				esac
+				;;
+			1)
+				if test "$line" = "----"
+				then
+					state=2
+				fi
+				;;
+			2)
+				description="$line"
+				break
+				;;
+		esac
+	done <"$path"
+
+	if test -z "$mansection"
+	then
+		echo "No man section found in $path" >&2
+		exit 1
+	fi
+
+	if test -z "$description"
+	then
+		echo >&2 "No description found in $path"
+		exit 1
+	fi
+
+	case "$description" in
+		"$command - "*)
+			text="${description#$command - }"
+
+			printf "linkgit:%s[%s]::\n\t" "$command" "$mansection"
+			case "$attributes" in
+				*" deprecated "*)
+					printf "(deprecated) "
+					;;
+			esac
+			printf "$text.\n\n"
+			;;
+		*)
+			echo >&2 "Description does not match $command: $description"
+			exit 1
+			;;
+	esac
+}
+
+source_dir="$1"
+build_dir="$2"
+shift 2
+
+for out in "$@"
+do
+	category="${out#cmds-}"
+	category="${category%.adoc}"
+	path="$build_dir/$out"
+
+	while read command command_category attributes
+	do
+		case "$command" in
+		"#"*)
+			continue;;
+		esac
+
+		case "$command_category" in
+		"$category")
+			format_one "$source_dir" "$command" " $attributes ";;
+		esac
+	done <"$source_dir/command-list.txt" >"$build_dir/$out+"
+
+	if cmp "$build_dir/$out+" "$build_dir/$out" >/dev/null 2>&1
+	then
+		rm "$build_dir/$out+"
+	else
+		mv "$build_dir/$out+" "$build_dir/$out"
+	fi
+done
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 8b9e692c599..b731c76e9e7 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -315,12 +315,12 @@ cmd_lists = [
 
 documentation_deps += custom_target(
   command: [
-    perl,
+    shell,
     '@INPUT@',
     meson.project_source_root(),
     meson.current_build_dir(),
   ] + cmd_lists,
-  input: 'cmd-list.perl',
+  input: 'cmd-list.sh',
   output: cmd_lists
 )
 
diff --git a/meson.build b/meson.build
index 8bab8f3481f..97753d2cfa7 100644
--- a/meson.build
+++ b/meson.build
@@ -779,7 +779,7 @@ endif
 # features. It is optional if you want to neither execute tests nor use any of
 # these optional features.
 perl_required = get_option('perl')
-if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers') or get_option('docs') != []
+if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers')
   perl_required = true
 endif
 
-- 
2.49.0.805.g082f7c87e0.dirty
Junio C Hamano· Apr 15, 2025, 16:32 UTC · re: Patrick Steinhardt · lore

Re: [PATCH 4/4] Documentation: stop depending on Perl to generate command list

Patrick Steinhardt <ps@pks.im> writes:
Show 28 quoted lines
> The "cmd-list.perl" script is used to extract the list of commands part
> of a specific category and extracts the description of each command from
> its respective manpage. The generated output is then included in git(1)
> to list all Git commands.
>
> The script is written in Perl. Refactor it to use shell scripting
> exclusively so that we can get rid of the mandatory dependency on Perl
> to build our documentation.
>
> The converted script is slower compared to its Perl implementation. But
> by being careful and not spawning external commands in `format_one ()`
> we can mitigate the performance hit to a reasonable level:
>
>     Benchmark 1: Perl
>       Time (mean ± σ):      10.3 ms ±   0.2 ms    [User: 7.0 ms, System: 3.3 ms]
>       Range (min … max):    10.0 ms …  11.1 ms    200 runs
>
>     Benchmark 2: Shell
>       Time (mean ± σ):      74.4 ms ±   0.4 ms    [User: 48.6 ms, System: 24.7 ms]
>       Range (min … max):    73.1 ms …  75.5 ms    200 runs
>
>     Summary
>       Perl ran
>         7.23 ± 0.13 times faster than Shell
>
> While a sevenfold slowdown is significant, the benefit of not requiring
> Perl for a fully-functioning Git installation outweighs waiting a couple
> of milliseconds longer during the build process.

I personally do not feel Perl such a drag but whether it is 10ms vs 75ms, as long as we won't run the script excessively (and either meson or make should be set-up to avoid unnecessary work already), I do not think a shell script being slightly slower than a Perl script is a big deal. Thanks for working on this.

Show 27 quoted lines
> diff --git a/Documentation/cmd-list.sh b/Documentation/cmd-list.sh
> new file mode 100755
> index 00000000000..fa90781f3c7
> --- /dev/null
> +++ b/Documentation/cmd-list.sh
> @@ -0,0 +1,104 @@
> +#!/bin/sh
> +
> +set -e
> +
> +format_one () {
> +	source_dir="$1"
> +	command="$2"
> +	attributes="$3"
> +
> +	path="$source_dir/Documentation/$command.adoc"
> +	if ! test -f "$path"
> +	then
> +		echo >&2 "No such file $path"
> +		exit 1
> +	fi
> +
> +	state=0
> +	while read line
> +	do
> +		case "$state" in
> +			0)
Style. label and "case" and "esac" align, just like ...
> +				case "$line" in
> +				git*\(*\)|scalar*\(*\))
> +					mansection="${line##*\(}"
... this one.
Show 6 quoted lines
> +					mansection="${mansection%\)}"
> +					;;
> +				NAME)
> +					state=1;;
> +				esac
> +				;;
> +	case "$description" in
> +		"$command - "*)
Likewise.
Show 5 quoted lines
> +			text="${description#$command - }"
> +
> +			printf "linkgit:%s[%s]::\n\t" "$command" "$mansection"
> +			case "$attributes" in
> +				*" deprecated "*)
Ditto.
Show 17 quoted lines
> +					printf "(deprecated) "
> +					;;
> +			esac
> +			printf "$text.\n\n"
> +			;;
> +		*)
> +			echo >&2 "Description does not match $command: $description"
> +			exit 1
> +			;;
> +	esac
> +}
> +
> +source_dir="$1"
> +build_dir="$2"
> +shift 2
> +
> +for out in "$@"
Let's omit 'in "$@"' when iterationg over "$@".
Show 25 quoted lines
> +do
> +	category="${out#cmds-}"
> +	category="${category%.adoc}"
> +	path="$build_dir/$out"
> +
> +	while read command command_category attributes
> +	do
> +		case "$command" in
> +		"#"*)
> +			continue;;
> +		esac
> +
> +		case "$command_category" in
> +		"$category")
> +			format_one "$source_dir" "$command" " $attributes ";;
> +		esac
> +	done <"$source_dir/command-list.txt" >"$build_dir/$out+"
> +
> +	if cmp "$build_dir/$out+" "$build_dir/$out" >/dev/null 2>&1
> +	then
> +		rm "$build_dir/$out+"
> +	else
> +		mv "$build_dir/$out+" "$build_dir/$out"
> +	fi
> +done
OK.
Patrick Steinhardt· Apr 16, 2025, 12:16 UTC · re: Patrick Steinhardt · lore

[PATCH v2 0/4] Drop Perl dependency in a couple of subsystems

Hi,

this patch series is a follow-up to my recent series that got rid of our strict dependency on Perl for running our test suite [1]. The goal of it is to remove the dependency on Perl in a couple more areas:

  - git-filter-branch(1)
  - git-request-pull(1)
  - Building our documentation.

With these converted you can get a fully-functioning Git distribution with only a small handful of features missing:

  - gitweb.
  - Scripts to import repositories from GNU Arch, CVS and Subversion.
  - git-send-email.
  - Our Perl bindings, obviously.
  - The netrc credential helper from "contrib/".

None of these feel critical for day-to-day usage of Git, so Perl is now a truly-optional dependency of Git.

The series is built on top of 485f5f86361 (The fourth batch, 2025-04-08) with ps/test-wo-perl-prereq at 7a7b6022670 (t5703: refactor test to not depend on Perl, 2025-04-03) merged into it.

Changes in v2:
  - Loosen up semantics when parsing "^{}" from git-ls-remote(1) so that
    the new behaviour matches the old one.
  - Fix indentation of a couple of case statements.
  - Remove explicit `in "$@"` in a for loop.
  - Link to v1: https://lore.kernel.org/r/20250415-b4-pks-drop-perl-v1-0-c6addf175858@pks.im
Thanks!
Patrick
[1]: <20250320-b4-pks-t-perlless-v1-0-b1eefe27ac55@pks.im>
---
Patrick Steinhardt (4):
      filter-branch: stop depending on Perl
      request-pull: stop depending on Perl
      Documentation: stop depending on Perl to massage user manual
      Documentation: stop depending on Perl to generate command list
 Documentation/Makefile      |   8 ++--
 Documentation/cmd-list.perl |  80 ----------------------------------
 Documentation/cmd-list.sh   | 104 ++++++++++++++++++++++++++++++++++++++++++++
 Documentation/fix-texi.perl |  15 -------
 Documentation/fix-texi.sh   |  21 +++++++++
 Documentation/meson.build   |   4 +-
 git-filter-branch.sh        |  37 ++++++++--------
 git-request-pull.sh         |  74 ++++++++++++++++---------------
 meson.build                 |   2 +-
 t/t5150-request-pull.sh     |   6 ---
 10 files changed, 191 insertions(+), 160 deletions(-)
Range-diff versus v1:
1:  d0aa04cb2e9 = 1:  b48e33ada07 filter-branch: stop depending on Perl
2:  0fc604b849d ! 2:  31f54bd63e4 request-pull: stop depending on Perl
    @@ git-request-pull.sh: fi
     +	while read sha1 ref
     +	do
     +		case "$ref" in
    -+		*"^{}")
    -+			ref="${ref%"^{}"}"
    ++		*"^"?*)
    ++			ref="${ref%"^"*}"
     +			deref=true
     +			;;
     +		*)
3:  75dd9df3d83 = 3:  788fb204917 Documentation: stop depending on Perl to massage user manual
4:  9c4b5c330b4 ! 4:  c770a80762b Documentation: stop depending on Perl to generate command list
    @@ Documentation/cmd-list.sh (new)
     +	while read line
     +	do
     +		case "$state" in
    -+			0)
    -+				case "$line" in
    -+				git*\(*\)|scalar*\(*\))
    -+					mansection="${line##*\(}"
    -+					mansection="${mansection%\)}"
    -+					;;
    -+				NAME)
    -+					state=1;;
    -+				esac
    -+				;;
    -+			1)
    -+				if test "$line" = "----"
    -+				then
    -+					state=2
    -+				fi
    -+				;;
    -+			2)
    -+				description="$line"
    -+				break
    ++		0)
    ++			case "$line" in
    ++			git*\(*\)|scalar*\(*\))
    ++				mansection="${line##*\(}"
    ++				mansection="${mansection%\)}"
     +				;;
    ++			NAME)
    ++				state=1;;
    ++			esac
    ++			;;
    ++		1)
    ++			if test "$line" = "----"
    ++			then
    ++				state=2
    ++			fi
    ++			;;
    ++		2)
    ++			description="$line"
    ++			break
    ++			;;
     +		esac
     +	done <"$path"
     +
    @@ Documentation/cmd-list.sh (new)
     +	fi
     +
     +	case "$description" in
    -+		"$command - "*)
    -+			text="${description#$command - }"
    ++	"$command - "*)
    ++		text="${description#$command - }"
     +
    -+			printf "linkgit:%s[%s]::\n\t" "$command" "$mansection"
    -+			case "$attributes" in
    -+				*" deprecated "*)
    -+					printf "(deprecated) "
    -+					;;
    -+			esac
    -+			printf "$text.\n\n"
    -+			;;
    -+		*)
    -+			echo >&2 "Description does not match $command: $description"
    -+			exit 1
    ++		printf "linkgit:%s[%s]::\n\t" "$command" "$mansection"
    ++		case "$attributes" in
    ++		*" deprecated "*)
    ++			printf "(deprecated) "
     +			;;
    ++		esac
    ++		printf "$text.\n\n"
    ++		;;
    ++	*)
    ++		echo >&2 "Description does not match $command: $description"
    ++		exit 1
    ++		;;
     +	esac
     +}
     +
    @@ Documentation/cmd-list.sh (new)
     +build_dir="$2"
     +shift 2
     +
    -+for out in "$@"
    ++for out
     +do
     +	category="${out#cmds-}"
     +	category="${category%.adoc}"

--- base-commit: b09b3d4791ed248fed65de18cd6d8c084f7f0280 change-id: 20250409-b4-pks-drop-perl-574571234e3c

Patrick Steinhardt· Apr 16, 2025, 12:16 UTC · re: Patrick Steinhardt · lore

[PATCH v2 1/4] filter-branch: stop depending on Perl

While git-filter-branch(1) is written as a shell script, the `--state-branch` feature depends on Perl to save and extract the object ID mappings. This can lead to subtle breakage though:

  - We execute `perl` directly without respecting the `PERL_PATH`
    configured by the distribution. As such, it may happen that we use
    the wrong version of Perl.
  - We install the script unchanged even if Perl isn't available at all
    on the system, so using `--state-branch` would lead to failure
    altogether in that case.

Fix this by dropping Perl and instead implementing the feature with shell scripting exclusively.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 git-filter-branch.sh | 37 +++++++++++++++++++------------------
 1 file changed, 19 insertions(+), 18 deletions(-)
Show changes to git-filter-branch.sh +19 −18
diff --git a/git-filter-branch.sh b/git-filter-branch.sh
index 3a51d4507c7..24fa317aaaa 100755
--- a/git-filter-branch.sh
+++ b/git-filter-branch.sh
@@ -295,15 +295,18 @@ then
 	if test -n "$state_commit"
 	then
 		echo "Populating map from $state_branch ($state_commit)" 1>&2
-		perl -e'open(MAP, "-|", "git show $ARGV[0]:filter.map") or die;
-			while (<MAP>) {
-				m/(.*):(.*)/ or die;
-				open F, ">../map/$1" or die;
-				print F "$2" or die;
-				close(F) or die;
-			}
-			close(MAP) or die;' "$state_commit" \
-				|| die "Unable to load state from $state_branch:filter.map"
+
+		git show "$state_commit:filter.map" >"$tempdir"/filter-map ||
+			die "Unable to load state from $state_branch:filter.map"
+		while read line
+		do
+			case "$line" in
+			*:*)
+				echo "${line%:*}" >../map/"${line#*:}";;
+			*)
+				die "Unable to load state from $state_branch:filter.map";;
+			esac
+		done <"$tempdir"/filter-map
 	else
 		echo "Branch $state_branch does not exist. Will create" 1>&2
 	fi
@@ -633,15 +636,13 @@ if test -n "$state_branch"
 then
 	echo "Saving rewrite state to $state_branch" 1>&2
 	state_blob=$(
-		perl -e'opendir D, "../map" or die;
-			open H, "|-", "git hash-object -w --stdin" or die;
-			foreach (sort readdir(D)) {
-				next if m/^\.\.?$/;
-				open F, "<../map/$_" or die;
-				chomp($f = <F>);
-				print H "$_:$f\n" or die;
-			}
-			close(H) or die;' || die "Unable to save state")
+		for file in ../map/*
+		do
+			from_commit=$(basename "$file")
+			to_commit=$(cat "$file")
+			echo "$from_commit:$to_commit"
+		done | git hash-object -w --stdin || die "Unable to save state"
+	)
 	state_tree=$(printf '100644 blob %s\tfilter.map\n' "$state_blob" | git mktree)
 	if test -n "$state_commit"
 	then
-- 
2.49.0.805.g082f7c87e0.dirty
Patrick Steinhardt· Apr 16, 2025, 12:16 UTC · re: Patrick Steinhardt · lore

[PATCH v2 2/4] request-pull: stop depending on Perl

While git-request-pull(1) is written as a shell script, for it to function we depend on Perl being available. The script gets installed unconditionally though, regardless of whether or not Perl is even available on the system. When it's not available, the `@PERL_PATH@` variable may be substituted with a nonexistent executable path and thus cause the script to fail.

Refactor the script so that it does not depend on Perl at all anymore.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 git-request-pull.sh     | 74 ++++++++++++++++++++++++++-----------------------
 t/t5150-request-pull.sh |  6 ----
 2 files changed, 40 insertions(+), 40 deletions(-)
Show changes to 2 files +40 −40

git-request-pull.sh, t/t5150-request-pull.sh

diff --git a/git-request-pull.sh b/git-request-pull.sh
index 775ba8ea11a..6a7b7936784 100755
--- a/git-request-pull.sh
+++ b/git-request-pull.sh
@@ -78,41 +78,47 @@ fi
 merge_base=$(git merge-base $baserev $headrev) ||
 die "fatal: No commits in common between $base and $head"
 
-# $head is the refname from the command line.
-# Find a ref with the same name as $head that exists at the remote
+find_matching_ref () {
+	while read sha1 ref
+	do
+		case "$ref" in
+		*"^"?*)
+			ref="${ref%"^"*}"
+			deref=true
+			;;
+		*)
+			deref=
+			;;
+		esac
+
+		if test "$sha1" = "${remote:-HEAD}"
+		then
+			echo "$sha1 $sha1"
+			break
+		fi
+
+		case "$ref" in
+		"${remote:-HEAD}"|*"/${remote:-HEAD}")
+			if test -z "$deref"
+			then
+				# Remember the matching unpeeled object on the
+				# remote side.
+				remote_sha1="$sha1"
+			fi
+
+			if test "$sha1" = "$headrev"
+			then
+				echo "${remote_sha1:-$headrev} $ref"
+				break
+			fi
+			;;
+		esac
+	done
+}
+
+# Find a ref with the same name as $remote that exists at the remote
 # and points to the same commit as the local object.
-find_matching_ref='
-	my ($head,$headrev) = (@ARGV);
-	my $pattern = qr{/\Q$head\E$};
-	my ($remote_sha1, $found);
-
-	while (<STDIN>) {
-		chomp;
-		my ($sha1, $ref, $deref) = /^(\S+)\s+([^^]+)(\S*)$/;
-
-		if ($sha1 eq $head) {
-			$found = $remote_sha1 = $sha1;
-			break;
-		}
-
-		if ($ref eq $head || $ref =~ $pattern) {
-			if ($deref eq "") {
-				# Remember the matching object on the remote side
-				$remote_sha1 = $sha1;
-			}
-			if ($sha1 eq $headrev) {
-				$found = $ref;
-				break;
-			}
-		}
-	}
-	if ($found) {
-		$remote_sha1 = $headrev if ! defined $remote_sha1;
-		print "$remote_sha1 $found\n";
-	}
-'
-
-set fnord $(git ls-remote "$url" | @PERL_PATH@ -e "$find_matching_ref" "${remote:-HEAD}" "$headrev")
+set fnord $(git ls-remote "$url" | find_matching_ref)
 remote_sha1=$2
 ref=$3
 
diff --git a/t/t5150-request-pull.sh b/t/t5150-request-pull.sh
index cb67bac1c47..270ce6ea487 100755
--- a/t/t5150-request-pull.sh
+++ b/t/t5150-request-pull.sh
@@ -7,12 +7,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 
 . ./test-lib.sh
 
-if ! test_have_prereq PERL
-then
-	skip_all='skipping request-pull tests, perl not available'
-	test_done
-fi
-
 test_expect_success 'setup' '
 
 	git init --bare upstream.git &&
-- 
2.49.0.805.g082f7c87e0.dirty
Patrick Steinhardt· Apr 16, 2025, 12:16 UTC · re: Patrick Steinhardt · lore

[PATCH v2 4/4] Documentation: stop depending on Perl to generate command list

The "cmd-list.perl" script is used to extract the list of commands part of a specific category and extracts the description of each command from its respective manpage. The generated output is then included in git(1) to list all Git commands.

The script is written in Perl. Refactor it to use shell scripting exclusively so that we can get rid of the mandatory dependency on Perl to build our documentation.

The converted script is slower compared to its Perl implementation. But by being careful and not spawning external commands in `format_one ()` we can mitigate the performance hit to a reasonable level:

    Benchmark 1: Perl
      Time (mean ± σ):      10.3 ms ±   0.2 ms    [User: 7.0 ms, System: 3.3 ms]
      Range (min … max):    10.0 ms …  11.1 ms    200 runs
    Benchmark 2: Shell
      Time (mean ± σ):      74.4 ms ±   0.4 ms    [User: 48.6 ms, System: 24.7 ms]
      Range (min … max):    73.1 ms …  75.5 ms    200 runs
    Summary
      Perl ran
        7.23 ± 0.13 times faster than Shell

While a sevenfold slowdown is significant, the benefit of not requiring Perl for a fully-functioning Git installation outweighs waiting a couple of milliseconds longer during the build process.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/Makefile      |   4 +-
 Documentation/cmd-list.perl |  80 ----------------------------------
 Documentation/cmd-list.sh   | 104 ++++++++++++++++++++++++++++++++++++++++++++
 Documentation/meson.build   |   4 +-
 meson.build                 |   2 +-
 5 files changed, 109 insertions(+), 85 deletions(-)
Show changes to 5 files +109 −85

Documentation/Makefile, Documentation/cmd-list.perl, Documentation/cmd-list.sh, Documentation/meson.build, meson.build

diff --git a/Documentation/Makefile b/Documentation/Makefile
index 6485d40f620..b109d25e9c8 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -317,8 +317,8 @@ cmds_txt = cmds-ancillaryinterrogators.adoc \
 
 $(cmds_txt): cmd-list.made
 
-cmd-list.made: cmd-list.perl ../command-list.txt $(MAN1_TXT)
-	$(QUIET_GEN)$(PERL_PATH) ./cmd-list.perl .. . $(cmds_txt) && \
+cmd-list.made: cmd-list.sh ../command-list.txt $(MAN1_TXT)
+	$(QUIET_GEN)$(SHELL_PATH) ./cmd-list.sh .. . $(cmds_txt) && \
 	date >$@
 
 mergetools-%.adoc: generate-mergetool-list.sh ../git-mergetool--lib.sh $(wildcard ../mergetools/*)
diff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl
deleted file mode 100755
index 0a0c1b3f611..00000000000
--- a/Documentation/cmd-list.perl
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/perl -w
-
-use File::Compare qw(compare);
-
-sub format_one {
-	my ($source_dir, $out, $nameattr) = @_;
-	my ($name, $attr) = @$nameattr;
-	my ($path) = "$source_dir/Documentation/$name.adoc";
-	my ($state, $description);
-	my $mansection;
-	$state = 0;
-	open I, '<', "$path" or die "No such file $path.adoc";
-	while (<I>) {
-		if (/^(?:git|scalar)[a-z0-9-]*\(([0-9])\)$/) {
-			$mansection = $1;
-			next;
-		}
-		if (/^NAME$/) {
-			$state = 1;
-			next;
-		}
-		if ($state == 1 && /^----$/) {
-			$state = 2;
-			next;
-		}
-		next if ($state != 2);
-		chomp;
-		$description = $_;
-		last;
-	}
-	close I;
-	if (!defined $description) {
-		die "No description found in $path.adoc";
-	}
-	if (my ($verify_name, $text) = ($description =~ /^($name) - (.*)/)) {
-		print $out "linkgit:$name\[$mansection\]::\n\t";
-		if ($attr =~ / deprecated /) {
-			print $out "(deprecated) ";
-		}
-		print $out "$text.\n\n";
-	}
-	else {
-		die "Description does not match $name: $description";
-	}
-}
-
-my ($source_dir, $build_dir, @categories) = @ARGV;
-
-open IN, "<$source_dir/command-list.txt";
-while (<IN>) {
-	last if /^### command list/;
-}
-
-my %cmds = ();
-for (sort <IN>) {
-	next if /^#/;
-
-	chomp;
-	my ($name, $cat, $attr) = /^(\S+)\s+(.*?)(?:\s+(.*))?$/;
-	$attr = '' unless defined $attr;
-	push @{$cmds{$cat}}, [$name, " $attr "];
-}
-close IN;
-
-for my $out (@categories) {
-	my ($cat) = $out =~ /^cmds-(.*)\.adoc$/;
-	my ($path) = "$build_dir/$out";
-	open O, '>', "$path+" or die "Cannot open output file $out+";
-	for (@{$cmds{$cat}}) {
-		format_one($source_dir, \*O, $_);
-	}
-	close O;
-
-	if (-f "$path" && compare("$path", "$path+") == 0) {
-		unlink "$path+";
-	}
-	else {
-		rename "$path+", "$path";
-	}
-}
diff --git a/Documentation/cmd-list.sh b/Documentation/cmd-list.sh
new file mode 100755
index 00000000000..077def3b721
--- /dev/null
+++ b/Documentation/cmd-list.sh
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+set -e
+
+format_one () {
+	source_dir="$1"
+	command="$2"
+	attributes="$3"
+
+	path="$source_dir/Documentation/$command.adoc"
+	if ! test -f "$path"
+	then
+		echo >&2 "No such file $path"
+		exit 1
+	fi
+
+	state=0
+	while read line
+	do
+		case "$state" in
+		0)
+			case "$line" in
+			git*\(*\)|scalar*\(*\))
+				mansection="${line##*\(}"
+				mansection="${mansection%\)}"
+				;;
+			NAME)
+				state=1;;
+			esac
+			;;
+		1)
+			if test "$line" = "----"
+			then
+				state=2
+			fi
+			;;
+		2)
+			description="$line"
+			break
+			;;
+		esac
+	done <"$path"
+
+	if test -z "$mansection"
+	then
+		echo "No man section found in $path" >&2
+		exit 1
+	fi
+
+	if test -z "$description"
+	then
+		echo >&2 "No description found in $path"
+		exit 1
+	fi
+
+	case "$description" in
+	"$command - "*)
+		text="${description#$command - }"
+
+		printf "linkgit:%s[%s]::\n\t" "$command" "$mansection"
+		case "$attributes" in
+		*" deprecated "*)
+			printf "(deprecated) "
+			;;
+		esac
+		printf "$text.\n\n"
+		;;
+	*)
+		echo >&2 "Description does not match $command: $description"
+		exit 1
+		;;
+	esac
+}
+
+source_dir="$1"
+build_dir="$2"
+shift 2
+
+for out
+do
+	category="${out#cmds-}"
+	category="${category%.adoc}"
+	path="$build_dir/$out"
+
+	while read command command_category attributes
+	do
+		case "$command" in
+		"#"*)
+			continue;;
+		esac
+
+		case "$command_category" in
+		"$category")
+			format_one "$source_dir" "$command" " $attributes ";;
+		esac
+	done <"$source_dir/command-list.txt" >"$build_dir/$out+"
+
+	if cmp "$build_dir/$out+" "$build_dir/$out" >/dev/null 2>&1
+	then
+		rm "$build_dir/$out+"
+	else
+		mv "$build_dir/$out+" "$build_dir/$out"
+	fi
+done
diff --git a/Documentation/meson.build b/Documentation/meson.build
index 8b9e692c599..b731c76e9e7 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -315,12 +315,12 @@ cmd_lists = [
 
 documentation_deps += custom_target(
   command: [
-    perl,
+    shell,
     '@INPUT@',
     meson.project_source_root(),
     meson.current_build_dir(),
   ] + cmd_lists,
-  input: 'cmd-list.perl',
+  input: 'cmd-list.sh',
   output: cmd_lists
 )
 
diff --git a/meson.build b/meson.build
index 8bab8f3481f..97753d2cfa7 100644
--- a/meson.build
+++ b/meson.build
@@ -779,7 +779,7 @@ endif
 # features. It is optional if you want to neither execute tests nor use any of
 # these optional features.
 perl_required = get_option('perl')
-if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers') or get_option('docs') != []
+if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers')
   perl_required = true
 endif
 
-- 
2.49.0.805.g082f7c87e0.dirty
Patrick Steinhardt· Apr 16, 2025, 12:16 UTC · re: Patrick Steinhardt · lore

[PATCH v2 3/4] Documentation: stop depending on Perl to massage user manual

The "fix-texi.perl" script is used to fix up the output of `docbook2x-texi`:

  - It changes the filename to be "git.info".
  - It changes the directory category and entry.

The script is written in Perl, but it can be rather trivially converted to a shell script. Do so to remove the dependency on Perl for building the user manual.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/Makefile      |  4 ++--
 Documentation/fix-texi.perl | 15 ---------------
 Documentation/fix-texi.sh   | 21 +++++++++++++++++++++
 3 files changed, 23 insertions(+), 17 deletions(-)
Show changes to 3 files +23 −17

Documentation/Makefile, Documentation/fix-texi.perl, Documentation/fix-texi.sh

diff --git a/Documentation/Makefile b/Documentation/Makefile
index 0d3a2c6bfe6..6485d40f620 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -398,9 +398,9 @@ user-manual.html: user-manual.xml $(XSLT)
 git.info: user-manual.texi
 	$(QUIET_MAKEINFO)$(MAKEINFO) --no-split -o $@ user-manual.texi
 
-user-manual.texi: user-manual.xml
+user-manual.texi: user-manual.xml fix-texi.sh
 	$(QUIET_DB2TEXI)$(DOCBOOK2X_TEXI) user-manual.xml --encoding=UTF-8 --to-stdout >$@+ && \
-	$(PERL_PATH) fix-texi.perl <$@+ >$@ && \
+	$(SHELL_PATH) fix-texi.sh <$@+ >$@ && \
 	$(RM) $@+
 
 user-manual.pdf: user-manual.xml
diff --git a/Documentation/fix-texi.perl b/Documentation/fix-texi.perl
deleted file mode 100755
index ff7d78f620a..00000000000
--- a/Documentation/fix-texi.perl
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/perl -w
-
-while (<>) {
-	if (/^\@setfilename/) {
-		$_ = "\@setfilename git.info\n";
-	} elsif (/^\@direntry/) {
-		print '@dircategory Development
-@direntry
-* Git: (git).           A fast distributed revision control system
-@end direntry
-';	}
-	unless (/^\@direntry/../^\@end direntry/) {
-		print;
-	}
-}
diff --git a/Documentation/fix-texi.sh b/Documentation/fix-texi.sh
new file mode 100755
index 00000000000..bc300f7b0f1
--- /dev/null
+++ b/Documentation/fix-texi.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+awk '
+	/^@setfilename/{
+		print "@setfilename git.info"
+		next
+	}
+	/^@direntry/{
+		direntry=1
+		print "@dircategory Development"
+		print "@direntry"
+		print "* Git: (git).           A fast distributed revision control system"
+		print "@end direntry"
+		next
+	}
+	/^@end direntry/{
+		direntry=0
+		next
+	}
+	!direntry
+'
-- 
2.49.0.805.g082f7c87e0.dirty

← back to recent threads