4

I have a script with two blocks: The first block is written in perl, the second block is written in bash

How do I switch shells (perl --> bash) in the middle of the script? Script attached below:

#! /usr/bin/perl -w
#
my @dirs = glob("*.frames");
foreach $dir (@dirs) {
   print "working on $dir\n";
   chdir $dir;
   my @digitfiles = glob ("RawImage_?.tif"); #need to make all files have 2-digit numbering for sort order to be correct
   foreach $file (@digitfiles) {
      my $newfile = $file;
      $newfile =~ s/RawImage_/RawImage_0/;
      rename $file,$newfile;
   }
   my $stackname = "../" . $dir . ".mrc";
   `tif2mrc -s *.tif $stackname`; #IMOD program to stack: -s option means treat input as signed INT
   chdir "../"; #go back up
}

#!/usr/bin/env bash
for f in *.mrc; do mv -- "$f" "${f%%.*}".mrc ; done

4 Answers4

27

Just rewrite the loop in Perl:

for my $file (glob '*.mrc') {
    ( my $newname = $file ) =~ s/\..*/.mrc/;
    rename $file, $newname or warn "$file: $!";
}
choroba
  • 47,233
  • 4
    Note that mv does (can do) more than just renaming. Though in this case, using perl's rename is probably closer to what the OP wants, as it doesn't have the issued that mv foo.bar.mrc foo.mrc would have if foo.mrc existed and was a directory. – Stéphane Chazelas Sep 18 '17 at 09:14
19

Ignoring the XY problem and to answer the question in the subject, you could do:

#! /usr/bin/perl
":" || q@<<"=END_OF_PERL"@;

# perl code here

exec "bash", "--", $0, @ARGV;
=END_OF_PERL@

# bash code here

bash will ignore the part up to the =END_OF_PERL@ line because it's:

: || something <<"=END_OF_PERL@"
...
=END_OF_PERL@

while the first line, in perl is just two strings (":" and q@quoted-string@) ORed together, so a no-op.

=END_OF_PERL@ in perl is the start of a pod (documentation) section so ignored by perl.

Note that if you want to pass variables from perl to bash, you have to export them to the environment (though you could also use arguments; here we're forwarding the list of arguments the perl script received to the bash script):

$ENV{ENV_VAR} = $perl_var;

The code assumes the perl part doesn't change the current working directory, as otherwise if $0 was a relative path, it would become invalid when passed to bash. To work around that, you can take an absolute path of the script at the start with:

#! /usr/bin/perl
":" || q@<<"=END_OF_PERL"@;
use Cwd "fast_abs_path";
my $script_path = fast_abs_path $0;

# perl code here

exec "bash", "--", $script_path, @ARGV;
=END_OF_PERL@

# bash code here

That's one example of a polyglot script, a script that is valid code in more than one language. If you enjoy those tricks, you can have a look at that more extreme one here or that codegolf Q&A.

perldoc perlrun shows another example of a sh+perl polyglot, but this time with sh being called first (for systems that don't support she-bangs).

7

You can't switch interpreters, but you can spawn a new shell process, then return to perl:

my $sh_code = <<'END_SH'
for f in *.mrc; do mv -- "$f" "${f%%.*}".mrc ; done
END_SH

system 'bash', '-c', $sh_code

Or you can replace the current perl process with a shell

exec 'bash', '-c', $sh_code

But as @choroba answers, perl is a general purpose language and can handle just about any task.

glenn jackman
  • 85,964
2

You could do everything in Perl, or everything in a shell script. Mixing languages is usually a bit messy though.

The shell script version may look something like this (with bash, for example):

#!/bin/bash

dirs=( *.frames )

for dir in "${dirs[@]}"; do
    printf 'Working on "%s"\n' "$dir"

    cd "$dir"

    digitfiles=( RawImage_?.tif )

    for file in "${digitfiles[@]}"; do
        newfile="RawImage_0${file#RawImage_}"
        mv "$file" "$newfile"
    done

    stackname="../$dir.mrc"
    tif2mrc -s *.tif "$stackname"

    cd ..
done

for f in *.mrc; do
    mv -- "$f" "${f%%.*}".mrc
done

Or, slightly more idiomatic (now with plain sh, but using a find that understands -execdir):

#!/bin/sh

echo 'Renaming TIFF files'

find *.frames -type f -name 'RawImage_?.tif' \
    -execdir sh -c 'mv "$1" "RawImage_0${1#RawImage_}"' sh {} ';'

for d in *.frames; do
    printf 'Processing "%s"...\n' "$d"
    tif2mrc -s "$d"/*.tif "${d%%.*}.mrc"
done

(both examples are tested only with regards to filename generation)

Change sh -c to sh -cx to get a bit of output for each renamed file, or add -print before -execdir.

With -execdir, the given shell command will be executed with the directory of the found file as its working directory. {} (and $1 within the subshell) will be the basename of the found file.

If tif2mrc needs to be run inside the directory of the TIFF files, then change the loop to

for d in *.frames; do
    printf 'Processing "%s"...\n' "$d"
    (cd "$d" && tif2mrc -s *.tif "../${d%%.*}.mrc" )
done

Note that in Perl, using backticks to execute a shell command will return the output of that command. You may instead use

system("tif2mrc -s *.tif $stackname");

if you're not interested in the output of tif2mrc.

Also, it's confusing to read a script or program that changes directories back and forth. It can additionally lead to unwanted things if a directory does not exist (the chdir(".."); would then take you up one directory level too high).

To properly do this, either execute the command with a displaced working directory, as in my last shell example, or properly check the return code of the initial chdir() in Perl (this would potentially still lead to code that may be hard to read and follow).

Kusalananda
  • 333,661
  • 3
    system("tif2mrc -s *.tif $stackname"); invokes a shell (because of the *), so the content of the $stackname perl variable is interpreted as shell code, making it a sort of command injection vulnerability (even without the *, it would be a command injection vulnerability). The system('tif2mrc', '-s', '*.tif', $stackname) version doesn't have the vulnerability, but the *.tif wouldn't be expanded. You'd need to use perl's glob(). – Stéphane Chazelas Sep 18 '17 at 11:48