Skip to content

Instantly share code, notes, and snippets.

@chunfeilung
Created October 26, 2025 23:19
Show Gist options
  • Select an option

  • Save chunfeilung/a6d4125e59c46f40ce9861d0477f57d1 to your computer and use it in GitHub Desktop.

Select an option

Save chunfeilung/a6d4125e59c46f40ce9861d0477f57d1 to your computer and use it in GitHub Desktop.
A helper script for executing commands inside the main container(s) of your Docker Compose projects
#!/usr/bin/perl
# SPDX-License-Identifier: EUPL-1.2
# Copyright (c) 2024-2025 Chun Fei Lung
use strict;
use warnings;
=pod
=head1 NAME
xx ("execs") - execute commands in the main container of a Compose project
=head1 SYNOPSIS
xx <command> Shorthand for "docker compose exec <service> <command>"
xx Shorthand for "docker compose exec <service> bash"
=head1 DESCRIPTION
A helper script that lets you easily execute commands inside the main container
of a Docker Compose project running in the current directory.
=cut
## main
# Entry point: a helper for "docker compose exec <service> <command>".
sub main {
my $argv_length = scalar @ARGV;
my $command;
if ($argv_length == 0) {
@ARGV = ("bash");
}
my @services;
@services = discover_service_names();
my $count = scalar @services;
if ($count == 0) {
print "No Docker Compose services were found in this directory.\n";
exit 1;
}
if ($count == 1) {
$command = "docker compose exec $services[0] @ARGV";
}
if ($count > 1) {
my $choice = display_menu(@services);
$command = "docker compose exec $services[$choice - 1] @ARGV";
}
system($command);
}
## discover_service_names
# Internal: find services with a build: section in the current Compose file'.
sub discover_service_names {
my $filename = find_compose_yaml();
if (!defined $filename || !-e $filename) {
print "Could not find a Compose file within the current path.\n";
exit 1;
}
open(my $fh, '<', $filename) or die "Cannot open file $filename: $!\n";
my @lines = <$fh>;
close($fh);
my @build_lines;
my @service_lines;
# First pass: collect build lines and determine the indent level of services
my $indent_level;
for (my $i = 0; $i < @lines; $i++) {
# Collect all lines containing "build:"
if ($lines[$i] =~ /build:/) {
push @build_lines, $i;
}
# Find first non-zero indent level (first line with leading whitespace)
if (!defined $indent_level && $lines[$i] =~ /^(\s+)\S/) {
$indent_level = length($1);
}
}
# Default to 2 spaces if no indent level was detected
$indent_level = 2 unless defined $indent_level && $indent_level > 0;
# Second pass: collect all service lines
for (my $i = 0; $i < @lines; $i++) {
if (length($lines[$i]) > $indent_level) {
my $prefix = substr($lines[$i], 0, $indent_level);
my $next_char = substr($lines[$i], $indent_level, 1);
if ($prefix =~ /^\s+$/ && $next_char =~ /[a-z]/) {
push @service_lines, $i;
}
}
}
my @service_names;
# Find the corresponding service name for each build line
foreach my $build_line (@build_lines) {
my $closest_lower = -1;
foreach my $service_line (@service_lines) {
if ($service_line < $build_line) {
$closest_lower = $service_line;
}
}
if ($closest_lower != -1) {
my $line = trim($lines[$closest_lower]);
my ($name) = split(/\s*:\s*/, $line, 2);
push @service_names, $name;
}
}
return @service_names;
}
## find_compose_yaml
# Internal: attempt to find a Compose file somewhere in the current path.
sub find_compose_yaml {
my $dir = '.';
my $max_levels = 50;
my @possible_compose_file_names = qw(
compose.yaml
compose.yml
docker-compose.yaml
docker-compose.yml
);
for (my $i = 0; $i < $max_levels; $i++) {
# Try to find a Compose file in this directory
foreach my $name (@possible_compose_file_names) {
my $path = "$dir/$name";
if (-e $path) {
return $path;
}
}
# Otherwise, try again in the parent directory
$dir = "../$dir";
}
return undef;
}
## trim
# Internal helper: remove leading/trailing whitespace from a string.
sub trim {
my ($string) = @_;
$string =~ s/^\s+|\s+$//g;
return $string;
}
## display_menu
# Internal: present a menu for @options and return the user's choice.
sub display_menu {
my @options = @_;
print "Where should '@ARGV' be executed?\n";
for (my $i = 0; $i < @options; $i++) {
print (($i + 1) . ". $options[$i]\n");
}
my $choice;
while (1) {
print "Enter the number of your choice: ";
# Attempt to read user input
{
my $input = <STDIN>;
if (!defined $input) {
$choice = "";
print "\n";
} else {
chomp $input;
$choice = $input;
}
}
if ($choice =~ /^\d+$/ && $choice > 0 && $choice <= @options) {
last;
} else {
print "Invalid choice. "
. "Please enter a number between 1 and " . scalar(@options)
. ".\n";
}
}
return $choice;
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment