PERLIMPORTS(1p)

πŸ‘‹ NAME ✨

okay so bestie, meet perlimports β€” A command line utility for cleaning up imports in your Perl code. that's it, that's the whole vibe. your use statements are about to get a glow-up πŸ’…


🌸 VERSION

version 0.000060


πŸŽ€ SYNOPSIS

okay so first things first, you're gonna make a config file called perlimports.toml and park it at the top level of your application or repo:

perlimports --create-config-file perlimports.toml

you can also do the whole config-file-with-a-leading-dot thing, lowkey iconic:

perlimports --create-config-file .perlimports.toml

and for system-wide defaults, drop a file in $XDG_HOME. something like this, bestie:

perlimports --create-config-file ~/.config/perlimports/perlimports.toml

once you've got that config file set up exactly how you like it, you can literally just stop using most of the command line switches β€” the only ones you still need are -i, --lint or --read-stdin. periodt.

now let's update a file in place. 🫢

bestie please make sure you can revert the file if you need to, because perlimports will not make a backup. no cap. it just edits. πŸ’€
perlimports -i foo.pl

lint a file? sure:

perlimports --lint foo.pl

in-place edits on directories:

perlimports -i lib t xt

lint directories:

perlimports --lint lib t xt

in-place edits on files and directories:

perlimports -i foo.pl lib t xt

run perlimports on a file and only print the results to STDOUT:

perlimports foo.pl

if some of your imported modules live in local directories, you can give it some hints about where to find them:

perlimports -i --libs t/lib,/some/dir/lib foo.pl

redirect output to a new file, go off:

perlimports foo.pl > foo.new.pl

πŸ’– DESCRIPTION

so this distribution ships the perlimports command line interface (CLI), and what it does is automate the cleanup and maintenance of your Perl use and require statements. it's loosely inspired by goimports (https://pkg.go.dev/golang.org/x/tools/cmd/goimports), and the whole goal is for it to become part of your linting and tidying workflow β€” same way you'd use perltidy or perlcritic. you get it. ✨

if you want the detailed deep-dive on the exact problems this tool is trying to solve, go watch this "Conference in the Cloud" talk from June 2021: Where did that Symbol Come From? (https://www.youtube.com/watch?v=fKqxdTbGxYY).

the slides for that talk are also available, and yes the command is exactly this:

curl -O https://raw.githubusercontent.com/oalders/presentations/main/slides/6-perlimports/remark.html && open remark.html

🌈 MOTIVATION

so like, lots of Perl modules helpfully export functions and variables by default. those are super handy shortcuts when you're writing a quick or small script, but the way code grows organically? they become a maintenance burden fr fr. and when code gets more complex, that complexity costs you in development time. flip side: reducing code complexity can speed development up. this tool wants to reduce complexity to further that goal. that's the whole point bestie. πŸ’œ

importing symbols by default or using export tags is a convenient shorthand for getting work done, but it means the developer has to retain knowledge of those defaults and tags in order to actually understand the code. perlimports wants you to be able to develop your code however you see fit, while still giving you a viable option to tidy your imports automatically. same energy as using perltidy to format your code β€” except now you're automating the "make my imports easier to understand" part. let's look at some examples. πŸ”₯

πŸ¦‹ Where is this function defined?

you might run into some code like this:

use strict;
use warnings;

use HTTP::Request::Common;
use LWP::UserAgent;

my $ua = LWP::UserAgent->new;
my $req = $ua->request( GET 'https://metacpan.org/' );
print $req->content;

so where does GET even come from?? if you're not familiar with HTTP::Request::Common, you might not realize that use HTTP::Request::Common has implicitly imported the functions GET, HEAD, PUT, PATCH, POST and OPTIONS into this block of code. sneaky.

what would happen if we used perlimports to import all needed functions explicitly? it might look something like this:

use strict;
use warnings;

use HTTP::Request::Common qw( GET );
use LWP::UserAgent ();

my $ua = LWP::UserAgent->new;
my $req = $ua->request( GET 'https://metacpan.org/' );
print $req->content;

the code above makes it immediately obvious where GET originates, which in turn makes it easier for us to look up its documentation. added bonus: it's also not importing HEAD, PUT or any of the other functions HTTP::Request::Common exports by default. so those functions literally cannot be used unwittingly later in the code. more understandable code for present day you, future you, and anyone else tasked with reading your code at some future point. such a slay. πŸ’…

keep in mind that this simple act can save a lot of time for developers who aren't intimately familiar with Perl and the default exports of many CPAN modules.

🌸 Are we even using all of these imports?

imagine the following import statement:

use HTTP::Status qw(
    is_cacheable_by_default
    is_client_error
    is_error
    is_info
    is_redirect
    is_server_error
    is_success
    status_message
);

...followed by 3,000 lines of code. like. how do you even know if all of these functions are actually being used? were they ever used? you could grep all of these function names manually, or you could remove them one by one by trial and error to see what breaks. it's doable, sure, but it does not scale well to scripts and modules with many imports, or to large code bases with many imports. having an unmaintained list of imports is preferable to implicit imports, ngl β€” but it would be helpful to automate maintaining this list.

perlimports can, in many situations, clean up your import statements and automate this maintenance burden away. which makes it easier for you to write clean code, which is easier to understand. obsessed. ✨

πŸ’– Are we even using all of these modules?

in cases where code is implicitly importing from modules, or where explicit imports are not being curated, it can be hard to discover which modules are no longer being used in a script, a module, or even a whole code base. removing unused modules from code can lead to gains in performance and a decrease in consumption of resources. and removing entire modules from your code base can decrease the number of dependencies you need to manage and decrease friction in your deployment process.

perlimports can remove unused modules for you, making dependency management so much easier. 🫢

πŸ‘‘ Enforcing a consistent style

a messy list of module imports makes your code harder to read. imagine this:

use Cpanel::JSON::XS;
use Database::Migrator::Types qw( HashRef ArrayRef Object Str Bool Maybe CodeRef FileHandle RegexpRef );
use List::AllUtils qw( uniq any );
use LWP::UserAgent    q{};
use Try::Tiny qw/ catch     try /;
use WWW::Mechanize  q<>;

perlimports turns the above list into:

use Cpanel::JSON::XS ();
use Database::Migrator::Types qw(
    ArrayRef
    Bool
    CodeRef
    FileHandle
    HashRef
    Maybe
    Object
    RegexpRef
    Str
);
use List::AllUtils qw( any uniq );
use LWP::UserAgent ();
use Try::Tiny qw( catch try);
use WWW::Mechanize ();

where possible, perlimports will enforce a consistent style of parentheses, and it'll also sort your imports and break up long lines. and like mentioned above, if some imports are no longer in use, perlimports will helpfully remove those for you. iconic behaviour, honestly. πŸŽ€

🧚 Import tags

import tags may obscure where symbols are coming from. while import tags provide a useful shorthand, they can contribute to code complexity by obscuring the origin of imported symbols. consider:

use HTTP::Status qw(:constants :is status_message);

the line above imports the status_message() function, as well as some other things via :constants and :is. but bestie... what exactly are these things?? we'll need to read the documentation to know for sure. 😭

perlimports can audit your code and expand the line above to list the symbols which you are actually importing. so the line above might now look something like:

use HTTP::Status qw(
    HTTP_ACCEPTED
    HTTP_BAD_REQUEST
    HTTP_CONTINUE
    HTTP_I_AM_A_TEAPOT
    HTTP_MOVED_PERMANENTLY
    HTTP_NO_CODE
    HTTP_NOT_FOUND
    HTTP_OK
    HTTP_PAYLOAD_TOO_LARGE
    HTTP_PERMANENT_REDIRECT
    HTTP_RANGE_NOT_SATISFIABLE
    HTTP_REQUEST_ENTITY_TOO_LARGE
    HTTP_REQUEST_RANGE_NOT_SATISFIABLE
    HTTP_REQUEST_URI_TOO_LARGE
    HTTP_TOO_EARLY
    HTTP_UNORDERED_COLLECTION
    HTTP_URI_TOO_LONG
    is_cacheable_by_default
    is_client_error
    is_error
    is_info
    is_redirect
    is_server_error
    is_success
    status_message
);

this is more verbose, yes, but grepping your code will now reveal to you where something like is_cacheable_by_default gets defined. you have increased the lines of code, but you have also reduced complexity. a trade bestie would make every single time. ⭐

πŸ’– COMMAND LINE PARAMETERS πŸ’–

okay so here's the whole menu of knobs you can twiddle, bestie. buckle up ✨


🌸 --create-config-file

This one wants a path to a .toml file that doesn't exist yet (it's gonna make it for you, no cap πŸŽ€).

--create-config-file perlimports.toml
--create-config-file .perlimports.toml

and if you want defaults for the whole system, you can drop one in $XDG_HOME, something like:

perlimports --create-config-file ~/.config/perlimports/perlimports.toml

every single one of those paths is a default location perlimports will go hunting for β€” unless you handed it a --config-file argument, in which case it's that one, periodt. πŸ’…

πŸ¦‹ --config-file

path to a perlimports config file, bestie. if you don't pass this, we go looking for a file named perlimports.toml or .perlimports.toml in the current directory, and after that we look for perlimports.toml inside XDG_CONFIG_HOME (usually something like $HOME/perlimports/perlimports.toml). and ngl if you want this whole autopilot thing OFF, that's what the next flag is for πŸ‘‡

πŸ™ˆ --no-config-file

stops perlimports from automatically grabbing a config file from any of the standard locations. use this when you want literally all of your behaviour defined on the command line, main character style. ✨

πŸŽ€ --filename|-f

absolute-or-relative path to a file (or a directory) to process.

--filename path/to/file

-f path/to/file

-f path/to/dir

lowkey important: if you don't give a --filename, we fall back to checking @ARGV for leftover args. so this:

perlimports --filename path/to/file

is the same thing as this:

perlimports path/to/file

and you can absolutely hand us a whole pile of file and dir names, we eat those for breakfast:

perlimports path/to/file path/to/other/file lib t xt

πŸ’€ --ignore-modules

a comma-separated list of module names this script should just... not touch. anything in this list stays exactly the same after processing. no edits, no nothing.

--ignore-modules Foo,Foo::Bar

πŸ’œ --ignore-modules-filename

absolute-or-relative path to a file with a list of module names to ignore (same behaviour as above). the pattern is one module name per line.

Foo
Foo::Bar

🌈 --ignore-modules-pattern

a regex that matches module names that should be ignored by this script. anything the regex catches stays unchanged after processing.

--ignore-modules-pattern '^(Foo|Foo::Bar)'

⭐ --ignore-modules-pattern-filename

absolute-or-relative path to a file containing a list of regexes that match modules to ignore (same behaviour as above, obviously πŸ’…). the pattern is one regular expression per line.

^Foo
^Foo::Bar

πŸ‘‘ --never-export-modules

comma-separated list of module names that should never export symbols. if we find them, we make sure they have an empty import list. so use Foo; becomes use Foo ();. iconic behaviour fr fr.

--never-export-modules Foo,Foo::Bar

πŸ“ --never-export-modules-filename

absolute-or-relative path to a file with a list of module names that should never export symbols (behaviour same as above, you know the drill). pattern = one module name per line.

Foo
Foo::Bar

πŸ”₯ --inplace-edit|-i

edit the file in place instead of printing the result to STDOUT. make sure you have a backup copy first!!

--inplace--edit
-i
⚠️ bestie I'm gonna say it twice because the manpage says it twice: this edits the file in place rather than printing the result to STDOUT. make sure you have a backup copy first.

🧚 --json

(Experimental). if you flip this on, linting errors get emitted as JSON objects, one object per error. it's meant to make life easier for editors trying to parse line numbers, column numbers and the error message. this flag can only be used in tandem with --lint.

🌸 --lint

pass this argument and perlimports switches into linter mode instead of tidier mode. failure messages (and success messages) get reported on STDERR. failures also return a non-zero exit code.

this is still a bit experimental, so the output format and the exit codes could change in a subsequent release. πŸ™ˆ

it also cannot be combined with tidying. so passing --lint --inplace-edit will generate an error. pick a lane, babe.

πŸ’… --indent

sets the indent width, in spaces, for qw() import lists that get split across multiple lines. it defaults to the value of perltidy's indent-columns setting (read from .perltidyrc if that file is present), or 4 if it's unset.

# --indent 2
use Foo qw(
  bar
  baz
);

πŸŽ€ --[no-]pad-brackets

when enabled, it adds whitespace inside the square brackets of test builder import lists. defaults to disabled.

# --pad-brackets
use Test::More import => [ qw( ok ) ];

# --no-pad-brackets
use Test::More import => [qw( ok )];

✨ --[no-]padding

--padding is enabled by default, so you only need to pass this arg if you want to be extra explicit about it. this setting adds whitespace inside the parentheses.

# --padding
use Foo qw( bar baz );

and the --no-padding arg lets you turn the extra padding inside parentheses off:

# --no-padding
use Foo qw(bar baz);

πŸ¦‹ --[no-]tidy-whitespace

--tidy-whitespace is enabled by default. what that means is: use statements get updated even when the only change is whitespace. turning it off can help cut down the churn when you run perlimports, especially if your codebase doesn't have automated tidying going on.

and listen β€” if you switched from --padding to --no-padding (or the other way around), you'll probably wanna make sure --tidy-whitespace is enabled too, so you can actually see those whitespace changes. πŸ‘€

🌈 --libs

a comma-separated list of module directories that aren't in your @INC.

--libs lib,t/lib

πŸ’€ --[no-]preserve-duplicates

when enabled, only one use statement per module gets preserved. it defaults to preserving duplicate statements.

so for example, when enabled, this text:

use Foo qw( bar );
use Foo qw (baz );

gets converted to:

use Foo qw( bar baz );

and if it's left disabled, the same thing above will probably get converted to:

use Foo qw( bar baz );
use Foo qw( bar baz );

which lets you manually decide how you actually want to handle the imports in question. use this setting with care, bestie 🫢

πŸ™ˆ --[no-]preserve-unused

when enabled, unused modules get removed. it defaults to preserving unused modules.

⚠️ enabling this may remove modules that are only there for preloading purposes, or that aren't being detected for other reasons. so yeah β€” use this setting with care.

πŸ‘‘ --range-begin

Experimental. first line of a range to tidy or lint. the line ranges begin at 1 (not 0). this will select the appropriate range of lines from STDIN. requires --read-stdin. mostly useful for editors. unless you're writing an editor plugin or extension, you can probably just ignore this one. πŸ’…

⭐ --range-end

Experimental. last line of a range to tidy or lint. line ranges begin at 1 (not 0). this selects the appropriate range of lines from STDIN. requires --read-stdin. mostly useful for editors. and again β€” unless you're building an editor plugin or extension, you can probably ignore this. ✨

πŸ“ --read-stdin

read statements to process from STDIN instead of processing the entire file. this is built for use by editors, like vim. check out the INTEGRATIONS section below for more on setting up an editor integration.

if this option is enabled, then --inplace-edit|-i is not available. sorry babe.

--read-stdin

🧚 --log-level|-l

generally only useful for debugging. notice notifies you about progress, like which file or snippet is currently being processed. info will generally log the errors that got swallowed while text was being processed. all levels are subject to change.

--log-level notice
--log-level info
-l notice
-l info

see <https://metacpan.org/pod/Log::Dispatch#LOG-LEVELS> for a list of available log levels. log output defaults to STDERR. check --log-filename if you'd rather log to a file instead.

πŸ’œ --log-filename

name of a file to redirect logs to, instead of STDERR.

🌸 --help

outputs a concise help menu with a summary of the available parameters.

--help

πŸŽ€ --verbose-help

includes the SYNOPSIS section from this page after printing the --help menu listed above. so it's the help menu plus bonus content. slay. ✨


πŸ’– ANNOTATIONS/IGNORING MODULES πŸ’–

so besides all the documented command line switches for ignoring modules, you can also just drop annotations right in your code. iykyk. 🫢

use Encode; ## no perlimports

that tells perlimports not to attempt a tidy of this line.

## no perlimports
use Encode;
use Cpanel::JSON::XS;
## use perlimports

use POSIX ();

and that tells perlimports not to tidy the two modules sitting inside the annotations.

⚠️ important caveat though: since perlimports needs to know as much as possible about what's going on in a file, these annotations don't prevent modules from being loaded. it's only a directive to leave the lines in the file unchanged after processing. got it? good πŸ’–

πŸ’– INTEGRATIONS πŸ’–

Okay bestie, listen β€” you are encouraged (like, heavily encouraged, the author is being so nice about it) to make this tool part of your automated tidying workflow. Here's some guidance on how to set that up. 🫢


✨ VIM ✨

If you're a vim girlie, you can pipe your import statements straight into perlimports. Look how easy:

:vnoremap <silent> im :!perlimports --read-stdin --filename '%:p'<CR>

So what that little guy does is let you visually select one or more lines of code and have them updated in place by perlimports. Once you've got the code selected, you hit im and boom πŸ’… β€” your imports get (re)formatted. Iconic.


🌸 VIM and ALE 🌸

If you use ALE with vim, you can drop something like this into your vim config. Heads up though: this function will save your buffer before running perlimports. No sneaky unsaved changes, bestie.

function! Perlimports(buffer) abort
  write
  return {
  \   'command': 'perlimports --read-stdin -f %s'
  \}
endfunction

let ale_fixers.perl = ['perlimports', 'perltidy']
execute ale#fix#registry#Add('perlimports', 'Perlimports', ['perl'], 'Tidy Perl imports')

πŸ¦‹ Emacs and Flycheck πŸ¦‹

Flycheck https://www.flycheck.org/ 35 and newer will just... Suggest changes from perlimports automatically if it finds that it's installed. No config, no drama. It's giving main character energy fr fr. ✨


πŸŽ€ precious πŸŽ€

If you're a https://github.com/houseabsolute/precious user, your config might look something like this:

exclude = [
    # Used by Dist::Zilla
    ".build",
    "App-perlimports-*",
    "blib",
    "inc",
    "test-data",
    # All of these are generated by Dist::Zilla
    "t/00-*",
    "t/author-*",
    "t/release-*",
    "xt/author",
    "xt/release",
]

[commands.perlimports]
type = "both"
include = [ "**/*.{pl,pm,t,psgi}" ]
cmd = [ "perlimports" ]
lint_flags = [ "--lint"]
tidy_flags = [ "-i"]
ok_exit_codes = 0
expect_stderr = true

[commands.perltidy]
type = "both"
include = [ "**/*.{pl,pm,t,psgi}" ]
cmd = [ "perltidy", "--profile=$PRECIOUS_ROOT/perltidyrc" ]
lint_flags = [ "--assert-tidy", "--no-standard-output", "--outfile=/dev/null" ]
tidy_flags = [ "--backup-and-modify-in-place", "--backup-file-extension=/" ]
ok_exit_codes = 0
lint_failure_exit_codes = 2
expect_stderr = true

So here's the tea: https://github.com/houseabsolute/precious runs plugins in order, which is why we put the perltidy config after perlimports. That's super handy because perlimports could introduce changes which will later be reverted by perltidy. By running them sequentially we can avoid false positives which might be generated by perlimports changing an include which perltidy might revert. Chef's kiss, honestly. πŸ‘‘

For an up to date example, go peep the config file which this repository uses: https://github.com/oalders/App-perlimports/blob/main/precious.toml


πŸ’œ Code::TidyAll πŸ’œ

If you're a Code::TidyAll user, you can configure perlimports as a GenericTransformer. Your config might look something like this:

[GenericTransformer perlimports]
select = **/*.{pl,pm,t,psgi}
ignore = .build/**/*
ignore = App-perlimports-*/**/*
ignore = blib/**/*
ignore = fatlib/**/*
ignore = inc/**/*
ignore = t/00-*
ignore = t/author-*
ignore = t/release-*
ignore = t/zzz-*
ignore = test-data/**/*
ignore = xt/**/*
ignore = xt/author/{pod-coverage,pod-spell,tidyall}.t
argv = --libs lib,t/lib --no-preserve-duplicates --no-preserve-unused --log-filename /tmp/perlimports.txt --log-level debug
cmd = perlimports
file_flag = -f
ok_exit_codes = 0
weight = 1

Note that in this case we've set the lowest possible weight. Why? Because we want perlimports to run before any other plugin which may transform the file. For example, you'll want perltidy to run after perlimports to avoid having to re-tidy files after your use statements have been rewritten. Order matters, bestie. 😌

If you want to use tidyall to run just perlimports you'll need to do something like:

tidyall --plugin "GenericTransformer perlimports" -a

For an up to date example, see the config file which this repository uses: https://github.com/oalders/App-perlimports/blob/main/tidyall.ini


πŸ“ RECIPES πŸ“

Included are some examples to use if you want to employ your own file finding logic or prefer not to use config files. If this does not apply to you, you can safely skip this section. No hard feelings. πŸ™ˆ

Running perlimports on test files

find t -type f |              \
  grep .t$ |                  \
  xargs perlimports           \
  --libs lib,t/lib            \
  --ignore-modules Test::More \
  --no-preserve-unused        \
  --no-preserve-duplicates    \
  --log-level debug           \
  -i

The above command:

  • finds all test files in ./t
  • pipes them to perlimports
  • adds lib and t/lib to @INC
  • ignores the Test::More module
  • removes unused modules
  • removes duplicated use statements
  • displays debugging info
  • edits files in place (-i)

Users of ack can make this a touch simpler:

ack -f --perltest           \
xargs perlimports           \
--libs lib,t/lib            \
--ignore-modules Test::More \
--no-preserve-unused        \
--no-preserve-duplicates    \
--log-level debug           \
-i

Running perlimports on modules

find lib -type f |         \
  grep .pm$ |              \
  xargs perlimports        \
  --libs lib               \
  --no-preserve-unused     \
  --no-preserve-duplicates \
  -i

The above command:

  • finds all .pm files in ./lib
  • pipes them to perlimports
  • adds lib to @INC
  • removes unused modules
  • removes duplicated use statements
  • edits files in place (-i)

We can also make this slightly shorter by using ack:

ack -f --perl lib        \
xargs perlimports        \
--libs lib               \
--no-preserve-unused     \
--no-preserve-duplicates \
-i

πŸ’€ CAVEATS πŸ’€

There are lots of shenanigans that Perl modules can get up to. This code will not find exports for all of those cases, but it should only attempt to rewrite imports which it knows how to handle. Please file a bug report in all other cases. Fr fr, if it breaks, tell the author β€” that's how it gets fixed. 🫢


🌈 SEE ALSO 🌈

  • Perl::Critic::Policy::TooMuchCode::ProhibitUnusedImport
  • Perl::Critic::Policy::TooMuchCode::ProhibitUnusedInclude
  • Perl::Critic::Policy::TooMuchCode::ProhibitUnusedConstant

πŸ‘‘ AUTHOR πŸ‘‘

Olaf Alders <olaf@wundercounter.com>


This software is copyright (c) 2020 by Olaf Alders.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.


aaaand that's a wrap on PERLIMPORTS(1p), bestie!! πŸŽ€ This lil masterpiece is running on perl v5.40.1 and it's dated 2026-05-11 β€” so go forth, tidy those imports, and remember: unused modules are NOT the vibe. πŸ’…βœ¨

πŸ“¦ where this page comes from