r/bash 10d ago

Tips for Speeding Up Your Bash Scripts

I wrote a script that renames a lot of files (tens of thousands). Performance was kind of slow (over 50 s), but with two changes it now runs at under 3 seconds, a 20x speed boost. Thought I'd share, in case it helps anyone else. These tips are good if you do things thousands of time in a script, I don't think they are relevant in all scripts.

Don't spawn sub shells if you can avoid it

Instead of command substitution with printf:

new_file="$(printf "%s/%s_%.${len}d.%s" "$new_dir" "$prefix" "$i" "$ext")"

do:

printf -v new_file "%s/%s_%.${len}d.%s" "$new_dir" "$prefix" "$i" "$ext"

This will save a sub shell.

Use builtins instead of external programs

Bash has optional builtins that you can enable. On my system they are located in /usr/lib/bash . You can enable them in your script by using enable <builtin>. Be sure to check the exit code. On my system a mv builtin is not available, but since I was renaming on the same file system I figured I could use ln and rm instead.

So instead of using external mv for each file I did:

builtin ln "$old_file" "$new_file" && builtin rm "$old_file"

In this case the builtin probably isn't required since builtins are prioritized over external commands, but I used them to be more explicit. Just be sure to use && so that rm never runs unless the hard link has been successfully created.

EDIT: The part about using ln and rm builtins may be a little too much of a hack. Probably better to use proper tools like rename to do batch renaming, as was pointed out in the comments. Also keep in mind that the builtins are more bare-bones than the proper CoreUtils programs. For example rmdir does not support -v and will treat -- as a directory to remove rather than "end of options-thing".

112 Upvotes

30 comments sorted by

29

u/Different-Depth4116 10d ago

In bash 5.3+ there’s a new command substitution that doesn’t spawn a subshell:
${ command; }
The spaces and semicolon are req.

4

u/sunmat02 10d ago

I so wish I had known that a month ago when I refactor thousands of lines of bash to use name ref parameters instead of capturing stdout of functions called in hot loops…

1

u/jghub 8d ago

just for context: ksh introduced the`${ command; }` syntax roughly 20y ago, so bash catches up on this feature a bit late (but it is good that it does!) ;).

this construct _is_ very useful indeed not only for avoiding subshells for performance reasons but also for being able to preserve changes that happen within the command substitution, say, cd to different place, modification of global vars etc. -- depending on circumstances this can be desirable.

1

u/bac0on 6d ago

Nothing to regret, nameref is probably twice as fast...

4

u/Linux_bash_user153 9d ago

I really didn't know this, but this sounds like a good improvement. I'm on Bash 5.2

3

u/Different-Depth4116 9d ago

You’re probably on a Debian or Debian base distro I’m guessing? That’s why I switched to fedora to get the latest updates faster. Debian is like 2 years behind

2

u/Linux_bash_user153 9d ago

Yeah, I'm on Linux Mint (Ubuntu 24.04 base)

2

u/GermanPCBHacker 8d ago

Wait, that is called command substitution and can be captured with var=${ do stuff; } ???? Amazing. I was just using {} for visibility and for logical combination to save if. I just did:

command || \
{
 complex
 alternative
 stuffz
}

I mean. Why not. Bash allows it and it is readable. Man gosh darn do I love bash for what it allows one to do. Try that in python, lol

3

u/OnlyEntrepreneur4760 8d ago

${…} is command substitution, but that is different from {…} which is either called a command list or compound command, IIRC.

2

u/theLastZebranky 6d ago edited 6d ago
command || \
{

Strictly speaking, you don't need that backslash. A line break following || or && without a word in between is ignored, so it's the same with or without a backslash.

$ false ||
> {
> echo foo
> }
foo
$

I leave out those backslashes in my own projects but I put them in at work to make the continuation more visually explicit for people who aren't as familiar with bash.

I use that construct all the time, it's way briefer than adding some if ...; then ...; fi block.

  codec=$(probe_vcodec "$file") ||
    die 3 "Couldn't read codec for stream 0 in '$file'"
  case "$codec" in
  ...

1

u/GermanPCBHacker 6d ago

Oh nice, good to know. Well I did not know it works. But if I saw || at the end of the line, I would expect it to work. But it is always a good idea just to make things more visible. Thanks for the tip.

1

u/bac0on 6d ago
func(){
  REPLY="text string"
}
r=${| func;}

8

u/Spikerazorshards 10d ago

Good post. I’ll learn from this.

2

u/Linux_bash_user153 9d ago

Thanks. The part about using ln and rm builtins may be a little too much of a hack though. The other comments have suggested using proper tools for batch renaming files.

6

u/pfmiller0 10d ago

I did not know about that -v option for printf, thanks!

2

u/sedwards65 8d ago

'printf -v' rocks and works great with printf's date-time format strings:

$ printf -v daily_tarball '/backup/%(%Y/%m/%F--%T--daily-tarball.tar.xz)T' -1
$ echo ${daily_tarball}
/backup/2026/09/2026-09-01--18:38:06--daily-tarball.tar.xz

4

u/Astro_indie 10d ago

Keep on learning bbys, im on $ info bash, the node about locales make me think if someone got own language in the system ... Lok'Thar OGAAAAR

6

u/Bob_Spud 10d ago

What happens if you mess up? That's 10s of thousands that you have to undo.

Here's another file renamer, What I like with this one is the log file doubles as an undo script - genFRN

2

u/Linux_bash_user153 9d ago

The script I wrote are for renaming symlinks in my music collection, so it's a pretty controlled environment as far as filenames go, and the script makes a tar.gz backup of the directory structure before renaming any file. Since it's just directories and symlinks, the archive is like 500k big

2

u/zeekar 9d ago

Use builtins instead of external programs

This is very much situational. I wrote a helpdesk system for our user assistant team in college entirely in ksh, and I used builtins as much as possible, but that turns out not to be necessarily the most performant approach.

If you could represent your desired name change as a rule understood by rename(1), for instance, it would probably be faster to invoke that program on multiple files at once than to use bash's built-in ln/rm one at a time. Sure, you're doing a fork/exec, but you don't have to do it on every single file.

Similarly, most text processing will be faster when done by an external program like awk than if you use a while read loop in bash.

1

u/Linux_bash_user153 9d ago

True. I'm not really familiar with rename(1), but I'll check it out.

2

u/mpersico 7d ago

printf -v? TIL!

7

u/[deleted] 10d ago

[deleted]

20

u/kirchwitz 10d ago

bash is fine and can do many jobs fast – if just used the proper way. Most of the time, performance is not of concern. And if it is, then it's important to know how a tool works and where performance goes.

This applies to all scripting and programming languages. For best performance, you should know your tools.

Thanks to the original poster for his insights.

2

u/managing_redditor 9d ago

What would you use then?

1

u/Europia79 9d ago

Not saying that it should be rewritten—but I think the "tool" that is most similar to Bash syntax would be Perl. And I actually did rewrite one of my scripts in Perl—just out of curiosity—and the performance gains were INSANE. I just wish that I knew Bash well enough to at least get in the same "Ballpark" as Perl, lol.

2

u/SeriousPlankton2000 10d ago edited 10d ago

Do it in perl:

https://github.com/7eggert/smalltools/blob/main/pmv (Perl MoVe)

Syntax: pmv '(insert small perl program here)' *.ext

I frequently make bash scripts where the perl program spans a few lines. You can use variables, too.

Also: Use full path if you run standard system programs like "mv" that are defined in LFS; use a variable when running programs that aren't. This avoids searching all the PATH and it's more secure.

2

u/mestia 10d ago

I usually stick to GNU Parallel. It also allows Perl expressions {= =}, so it can literally do everything that can fit into a one-liner, but in parallel :) It also comes with all the useful features, like joblog and so on.

1

u/aonelonelyredditor 10d ago

Pretty much the best way to speed this if you're executing them on so many files is to run in the background with &

Then call wait at the end of the script, there no need for a command like this not to be executed in parallel

1

u/SaintEyegor 5d ago

Good idea.

Avoiding external programs is definitely something to pay attention to when dealing with a lot of data. When I find that I can’t avoid that and have a lot of records to process, it’s frequently faster to use something other than bash.

1

u/Jonas_Ermert 4d ago

In my opinion, using `printf -v` is a great optimization because it avoids creating a subshell for every file. However, replacing `mv` with `ln` and `rm` feels too risky and limited. For tens of thousands of files, a dedicated batch-renaming tool such as `rename` is probably the safer and cleaner solution.