5

I have a file called file.txt containing:

MAL TIRRUEZF CR MAL RKZYIOL EX MAL OIY UAE RICF "MAL ACWALRM DYEUPLFWL CR ME DYEU MAIM UL IZL RKZZEKYFLF GH OHRMLZH"

I'd like the characters replaced as follows:

M = T
A = H
L = E
C = O
R = F
E = I
X = S
(Any other letter) = _
(Anything else) = (itself)

I have the fixed characters covered with:

tr MALCREX THEOFIS < file.txt

Or:

sed 'y/MALCREX/THEOFIS/' < file.txt

But how could I enforce the last two rules I mentioned?

5
  • 1
    Your example is just the trivial, sunny-day case where every "old" letter maps to a "new" letter that isn't also itself an "old" letter. If A = B and B = C, etc. can occur in your real data then include that in your example in a way that demonstrates if, given CAB as input for example, the result should be CBC or CCC.
    – Ed Morton
    Sep 24, 2021 at 16:46
  • 2
    Hmm, the OPs account seems to have been deleted so I guess we'll never know what they wanted and never get an answer accepted :-(.
    – Ed Morton
    Sep 24, 2021 at 16:47
  • @jubilatious1, for one, the question already stated that "[they] have the fixed characters covered with: tr MALCREX THEOFIS" and tr does only one translation per character, i.e. it maps L to E, period, not L => E => I. (I don't know how you'd go from E to E using those rules, though.) Also, if the rules were to be applied in sequence, even to already-translated characters, then it would seem to me the "any other letter => _" rule would eat all characters (E would translate to I, and THOFIS don't appear on the left side, so would match the "any other" condition)
    – ilkkachu
    Sep 28, 2021 at 9:06
  • In any case, a translation where all characters are translated at most one time is more general, since it can do swaps like "A => B; B => A", which are impossible if the rules are applied in sequence. And even if L => E => I is intended, it can be achieved by just fixing the table so that there's the L => I rule explicitly.
    – ilkkachu
    Sep 28, 2021 at 9:10
  • @ilkkachu Deleted above because the OP posted tr and sed code that he/she was happy with. The real question is how rule 2 and rule 3 apply to previously substituted characters. Also, you could interpret the 'any other letter' condition as just that--anything other than the 13 letters not previously mentioned--which obviates the question of whether they've been already-translated or not. Sep 28, 2021 at 10:06

5 Answers 5

12

I think you could use the fact that for many practical implementations, if a character repeats in the first set to tr, the last instance takes effect. Combined with the repeat syntax, you could do it without having to explicitly list the letters that don't appear in your transformation table.

With the GNU version of tr, and whatever FreeBSD based one I have on my Mac, this:

tr 'A-ZMALCREX' '[_*26]THEOFIS'

turns

MAL TIRRUEZF CR MAL RKZYIOL EX MAL OIY UAE RICF "MAL ACWALRM DYEUPLFWL CR ME DYEU MAIM UL IZL RKZZEKYFLF GH OHRMLZH"

into

THE __FF_I__ OF THE F_____E IS THE ___ _HI F_O_ "THE HO_HEFT __I__E__E OF TI __I_ TH_T _E __E F___I___E_ __ __FTE__"

Of course that assumes that A-Z produces exactly 26 characters, and I'm not sure if that applies in every locale with every tr implementation. It should work in the C locale, and e.g. the GNU version of tr doesn't support anything but raw 8-bit characters anyway.

The above doesn't work in Busybox, but that appears to be because it doesn't support the repetition syntax. There, you have to do it manually:

busybox tr 'A-ZMALCREX' '__________________________THEOFIS'

(thats 26 copies of the underscore)

Having a repeat character override the earlier instance of the same comes naturally for a simple table-based implementation. If your tr is implemented differently, you'll need to use the solutions from other answers.

0
6

Slightly longer than a couple of the other suggestions, but possibly easier to understand.

First suggestion: map the unwanted alphabetics to _ and then transpose the remaining set.

tr BDFGHIJKNOPQSTUVWYZ _ <file | tr MALCREX THEOFIS
THE __FF_I__ OF THE F_____E IS THE ___ _HI F_O_

Second suggestion: do it all in the one command. (GNU and BSD tr implicitly repeat the last character of the substitution target (_) as necessary for all unmapped characters in the substitution source map, but this behaviour is noted by POSIX simply as unspecified.)

tr MALCREXBDFGHIJKNOPQSTUVWYZ THEOFIS_ <file
THE __FF_I__ OF THE F_____E IS THE ___ _HI F_O_
0
5

I propose this perl alternative:

$ perl -pe 's/(?![MALCREX])[A-Z]/_/g;y/MALCREX/THEOFIS/' file 
THE __FF_I__ OF THE F_____E IS THE ___ _HI F_O_ "THE HO_HEFT __I__E__E OF TI __I_ TH_T _E __E F___I___E_ __ __FTE__"

It performs a lookahead assertion finding all characters in the range A-Z except MALCREX, Then performs the substitution as in your sed command.

As commented by Stéphane Chazelas, this solution has the advantage that the [A-Z] can be replaced with \w or \pL (and maybe add -Mopen=locale to handle all the characters in the locale) so it can handle all kinds of letters.


Another approach is as suggested by the comment from Uncle Billy:

perl -pe y/MALCREXA-Z/THEOFIS_/
6
  • 1
    How is that different from just perl -pe y/MALCREXA-Z/THEOFIS_/ ?
    – user313992
    Sep 24, 2021 at 7:43
  • @UncleBilly that I overcomplicated it? :-) . So if you don't mind, I'll edit my answer using your solution. Sep 24, 2021 at 14:52
  • 1
    @UncleBilly, a difference is that in the first, the [A-Z] can be replaced with \w or \pL (and maybe add -Mopen=locale to handle all the characters in the locale) so it can handle all kinds of letters. Sep 24, 2021 at 15:36
  • @StéphaneChazelas added your commentary to the answer. Sep 24, 2021 at 15:41
  • 1
    @StéphaneChazelas Oh better don't go there. You'll also have to handle combining marks (at least!) and decide what a "letter" actually is. And unfortunately the OP isn't here anymore to explain the purpose of this exercise.
    – user313992
    Sep 24, 2021 at 17:38
3

Using perl : as recommended in the comments @ImHere , the -C option is being added for uni code support.

perl -C -pe  's{\w+}{$& =~ tr/MALCREX/_/cr =~ y/MALCREX/THEOFIS/r}ge' file

Another way can be:

perl -pe 's{\w}{tr/MALCREX/THEOFIS/||s/./_/ for$a=$&;$a}ge' file

GNU sed can do it (change \w ---> [[:upper:]]) if you want POSIX behavior plus change the \n on the rhs of s/// to a literal escaped newline.

sed -e '
  s/\w/&\n/g
  s/[^MALCREX]\n/_/g
  y/MALCREX/THEOFIS/
  s/\n//g
' file

GNU sed

sed -Ee '
  s/[MALCREX]/&\n/g
  :a
    s/[A-Z]([^\n]|$)/_\1/
  ta
  y/MALCREX/THEOFIS/
  s/\n//g
' file
0
1

Using Raku (formerly known as Perl_6)

raku -pe 'tr/MALCREX/THEOFIS/; s:g/ <+:Uppercase_Letter - [THEOFIS]> /_/;' 

Sample Input:

MAL TIRRUEZF CR MAL RKZYIOL EX MAL OIY UAE RICF "MAL ACWALRM DYEUPLFWL CR ME DYEU MAIM UL IZL RKZZEKYFLF GH OHRMLZH"

Sample Output (running Raku code at top):

THE TIFF_I_F OF THE F___IOE IS THE OI_ _HI FIOF "THE HO_HEFT __I__EF_E OF TI __I_ THIT _E I_E F___I__FEF _H OHFTE_H"

One advantage of Raku for tackling this question is that Unicode is supported by default (not addressed in this answer, however).

The second statement in the code above uses a global s/// substitution with a bespoke character class <+:Uppercase_Letter - [THEOFIS]>, which can be expressed more simply as <+:Lu - [THEOFIS]>. As noted by @Stéphane Chazelas in the comments the :Lu can be replaced with :Letter or [\w] so it can handle all kinds of letters.

For the simple case presented (only " doublequote and space non-alnum characters), the second statement in the code above can written using tr/// with the :complement adverb. The statement tr:c/THEOFIS" /_/;, adds the " doublequote and space characters to THEOFIS list. (In other words, 'Take the :complement of all the characters between the first two /…/ and change them to the character listed between the second two /…/ , which in this case is _ underscore.' )

raku -pe 'tr/MALCREX/THEOFIS/; tr:c/THEOFIS" /_/;' 

Sample Output:

THE TIFF_I_F OF THE F___IOE IS THE OI_ _HI FIOF "THE HO_HEFT __I__EF_E OF TI __I_ THIT _E I_E F___I__FEF _H OHFTE_H"

EDITED:

Breaking the transliteration into two steps as I've done above means I run the danger of overwriting a 'final' character with the second step. To avoid this, I can convert to lowercase characters in both steps, which produces the same output seen by others (but--see below).

Finally, it looks as though the OP made an error in their posted transliteration table, since the updated table/code (below) gives much more sensible output:

raku -pe 'tr/MALCREX/theisof/; tr:c/theisof" /_/;'

Updated Output:

the __ss_o__ is the s_____e of the ___ _ho s_i_ "the hi_hest __o__e__e is to __o_ th_t _e __e s___o___e_ __ __ste__"

https://docs.raku.org/language/regexes#Predefined_character_classes
https://raku.org

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy