-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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