g | x | w | all
Bytes Lang Time Link
nanAArch64 machine code250607T030425Z鳴神裁四点一号
039AWK241028T150842Zxrs
028Uiua241028T021139Znoodle p
054J241027T233154ZConor O&
075JavaScript Node.js241017T054212Ztsh
049JavaScript Node.js241017T060415Zl4m2
073Python241016T122302Zmovatica
099Bash241017T191033ZIvan
093Java JDK241016T133837ZmastaH
079JavaScript Node.js241017T013857ZJake
024Charcoal241016T180034ZNeil
026Retina 0.8.2241016T174935ZNeil
070C gcc241016T131230Zjdt
043Perl241016T092429ZToto
030Perl 5 pl241016T145635ZXcali
021Japt241016T100808ZShaggy
016Vyxal241016T080412Zlyxal
02205AB1E241016T073412ZKevin Cr
068Google Sheets241016T071528Zdoubleun

AArch64 machine code, .text: 100 + .data: 6 = 106 bytes

Disassembly of section .text:

0000000000000000 <f-0x4>:
   0:   38001509        strb    w9, [x8], #1

0000000000000004 <f>:
   4:   38401409        ldrb    w9, [x0], #1
   8:   34000289        cbz     w9, 58 <f+0x54>
   c:   7101e822        subs    w2, w1, #0x7a
  10:   3a5a4848        ccmn    w2, #0x1a, #0x8, mi     // mi = first
  14:   1a9f47e2        cset    w2, pl  // pl = nfrst
  18:   4a021422        eor     w2, w1, w2, lsl #5
  1c:   7101e92a        subs    w10, w9, #0x7a
  20:   3a5a4948        ccmn    w10, #0x1a, #0x8, mi    // mi = first
  24:   1a9f47ea        cset    w10, pl // pl = nfrst
  28:   4a0a152a        eor     w10, w9, w10, lsl #5
  2c:   6b0a005f        cmp     w2, w10
  30:   54fffe81        b.ne    0 <f-0x4>  // b.any
  34:   1000000a        adr     x10, 0 <f-0x4>
  38:   39000549        strb    w9, [x10, #1]
  3c:   79400149        ldrh    w9, [x10]
  40:   78002509        strh    w9, [x8], #2
  44:   14000002        b       4c <f+0x48>
  48:   38001509        strb    w9, [x8], #1
  4c:   38401409        ldrb    w9, [x0], #1
  50:   35ffffc9        cbnz    w9, 48 <f+0x44>
  54:   d65f03c0        ret
  58:   10000000        adr     x0, 0 <f-0x4>
  5c:   39000401        strb    w1, [x0, #1]
  60:   17fffffb        b       4c <f+0x48>

Contents of section .data:

 0000 283f2900 5f3f    (?)._?

Synopsis

adr x0,.L.string
adr x1,.L.char
ldrb w1,[x1]
adr x8,.L.buf // Output
bl f

mov x0,1
adr x1,.L.buf
mov x2,1024
mov x8,64
svc 0

mov x0,0
mov x8,93
svc 0

.data
.L.string: .asciz "Search"
.L.char = 'S'                                                               
.bss
.L.buf: .fill 1024

Source

/// fn (x0: [*:0]const u8, w1: u8, x8: [*:0]u8) void
/// Writes result to x8[0..x0.len+4]
/// Expects @intFromPtr(x8) + x0.len < @intFromPtr(x0) or
///   @intFromPtr(x8) >= @intFromPtr(x0) + x0.len
.globl f

1:
strb w9,[x8],1      // Copy input character to output memory
f:
ldrb w9,[x0],1      // Get one input character
cbz w9,.L.NotFound  // Is it '\0'?

// w2 := toUpper(w1)
subs w2,w1,'z'
ccmn w2,26,8,mi
cset w2,pl
eor w2,w1,w2,lsl 5

// w10 := toUpper(w9)
subs w10,w9,'z'
ccmn w10,26,8,mi
cset w10,pl
eor w10,w9,w10,lsl 5

cmp w2,w10
b.ne 1b

.L.Found:
adr x10,.L.Found.msg  // .ascii "_?"
strb w9,[x10,1]       // Replace "?" with found character
ldrh w9,[x10]         // Get two characters
strh w9,[x8],2        // Store to output memory
b .L.CopyRest

1:
strb w9,[x8],1
.L.CopyRest:
ldrb w9,[x0],1
cbnz w9,1b
ret

.L.NotFound:
adr x0,.L.NotFound.msg  // .asciz "(?)"
strb w1,[x0,1]          // Replace "?" with w1 character
b .L.CopyRest

.data
.L.NotFound.msg:
.asciz "(?)"
.L.Found.msg:
.ascii "_?"

How to test

Here is the base64 of my tarball. Save as dist.tar.xz.base64:

/Td6WFoAAATm1rRGBMCDB4BQIQEWAAAAAAAAALC23ivgJ/8De10AMwuKYA5QJwj87SsS4vWGI5bQ
b8vLTTeLwCWMuNzy+otbbIGHc5Fc+wEawk5h+DcwF2OU17apMJXXHpea26zAfUL/eYeRgkiEAuHq
Wd/JABJWTgTxcEQUcvvqieYabc580J40llyn2dxKxG2UAk/Lj95UKwor3cNw5T+5ImnjUUWuWXxN
BamlixXNPLFDk2vMdjoXxIWXYXK+192o0mqUtS+nQnxHloTlKsL3Sxd3hJL/y+kadMnct9i0Hmgo
IHpNegK6h7MuZRwN4Hlr4RAFGdoO2aCunHg6PqTF6KSDgbefM1QTWAq83fHuLI8EHelJzUrSm6+A
TPZNb+6VKt1YIaaO3imEVw5XgHychHswdbJ8jARbRkXG2oxpERhXKbmKYcpNZoovBCt1uiUbAen2
fbsu2Wjng4vt6xENjaUCZ+I4dMXfs3FVCLnPVU3q9o7nlaV5UrwT6iSH3pihgSmkeVnzocWrNWIZ
5EOrHmhxlkVLb4lv08IdU/KPN0VXweXPLzef8i8b8T2EkBeofNhXDoUfSTwION0Ywjjs0hopT466
qd+42c9m22SQioLyijefaV1E2RDUTJis+ewkFE1hzn9jRuDJA8vN7eJXUtUHNfP6pAoxa1OUwZF+
gkrvyJhQgscCrNdWzlV1Nh+i00tdhUrr1DU3iY9x4P8nPzAaqr3ajmGbPjUTCbrAmlNen6Lmd0BD
17jb8dc+AzGZtWmOVB+psqem7j6El8Ld5VBqXyv8tfHo3vRbU2tDnsT+zEO/72LG93VgbgZRYSfx
q9CTbYCQYI9UtrIo50qfe/Cgn3RJuXE2ba8SLcJyvSnD8KTsUHWXivP9jDdiTPKlB5ZniQW5RKIJ
7zyfUvf1DUNlUtpBTMLGzkp3KCZc1ezDW+rpd14MAZxYi6x5m/5VTNLXZ4efuvBAfeIzk6SRiNbe
esfAxDOMjR/HTubgi952tlLAZ+iKZF45+R+rVr6i3FeaXIo0EnW+dj8yMrBwJXC1mOyF/x7Noa6f
w95x7lsUOseVGHq6WueICnNNnZAVYNHzqabrVPqDBbXSd69BBIeoK+7zphYzDgVZ32nq8Bv5uyhi
/o0zJy1AbmY1w4ES1SPmD7aDh4GTnuBVvRbFP5yuZYYz7Au4X8o9SJpZzW9QrOUWLXwMZKus9Jx4
nPDBjApK8vQLx00VVmuAu60AAACuqdWqVROIjgABnweAUAAA0DSgHLHEZ/sCAAAAAARZWg==

Decode it and extract with mode Jx:

$ cat dist.tar.xz.base64 | base64 -d | tar Jxf -

You will have four files:

f.s
main.s
testcases.txt
test.sh

Assembled and link two files with your assembler and linker:

$ as -o f.o f.s && as -o main.o main.s && ld -o main f.o main.o

Now you have an executable. Finally run a test; you will have the following result:

$ ./test.sh | tr -d '\0'
./main Search S
_Search

./main Translate N
Tra_nslate

./main Cut X
Cut(X)

./main Snap 10th 1
Snap _10th

./main Snap 50th 2
Snap 50th(2)

./main Excavate E
_Excavate

./main Cut(X) (
Cut_(X)

./main Cut(X) X
Cut(_X)

./main Cut(X) )
Cut(X_)

./main Cut(X) V
Cut(X)(V)

./main Test q
Test(q)

./main Test )
Test())

History

-4 bytes for position of label f

-4 bytes for toUpper() algorithm improvement

AWK, 39 bytes

$1~$2&&sub($2,"_"$2)||$0=$1"($2)",$0=$1

This only seems to work on gawk, whereas mawk and nawk will throw you an error. Also, it runs on TIO, but prints out gibberish :-p

Uiua, 28 bytes

◌⍥:⊃∈(⊂⊂⊙@_⊃↙↘⊗:)⊃∩⌵⟜$"_(_)"

Try it: Uiua pad

⊃∩⌵⟜$"_(_)" puts on the stack the two uppercased inputs, the first input, and "first(second)".

(⊂⊂⊙@_⊃↙↘⊗:) first index of letter, fork split drop, join with underscore between.

◌⍥:⊃∈ check membership of the (both uppercased previously) letter in the string, flip the two stack items that many times, and pop. (Basically: if member, pop the parenthesis thing, otherwise pop the underscore thing)

J, 54 bytes

(F{.[),](')',~'(',[)`('_',])@.(*@#@])[}.~F=:i.&tolower

Try it online!

I tried for hours to make this feel good. My second attempt, which tried to avoid the F=: assignment, was much longer, even after much golfing:

;(,')',~'(',])&>/@[`(]({.,'_',}.)0>@{[)@.(#@(0>@{[)*@-])i.&tolower
;(,')',~'(',])&>/@[`(]({.,'_',}.)F)@.(#@(F=:0>@{[)*@-])i.&tolower
,(}:,')',~'(',{:)@[`(]({.,'_',}.)}:@[)@.(#@}:@[*@-])i.&tolower
i.&tolower(}:,')',~'(',{:)@]`(({.,'_',}.)}:@])@.(#@}:@]*@-[),
i.&tolower(}:,')',~'(',{:)@]`([({.,'_',}.)}:@])@.(#@}:@]>[),
i.&tolower(}:,')',~'(',{:)@]`([({.,'_',}.)}:@])@.(<#@}:@]),

Agenda is probably not the way to go here, maybe there's more bytes to be saved by a completely different approach.

JavaScript (Node.js), 75 bytes

c=>g=s=>s?s[U='toUpperCase']()[0]==c[U]()?'_'+s:s[0]+g(s.slice(1)):`(${c})`

Try it online!

JavaScript (Node.js), 49 bytes

s=>c=>`${s}(${c})`.replace(/((.).*).\2.$/i,'_$1')

Try it online!

From Arnauld's deleted answer

-7B from tsh

Python, 73 bytes

lambda s,c:[s[:(i:=s.lower().find(c.lower()))]+'_'+s[i:],s+f'({c})'][i<0]

Attempt This Online!

Ungolfed for better understanding:

def f(s,c):
 # case insensitive search, return index or -1 if not found
 i = s.lower().find(c.lower())
 # use the index to select the correct string
 # first option, selected if found, inserts a _ at position i
 return [s[:i]+'_'+s[i:], s+f'({c})'][i<0]

80 bytes using regexp

lambda s,c:(t:=re.sub(r'(?=\u%04X)'%ord(c),'_',s,1,2))+f'({c})'*(s==t)
import re

Attempt This Online!

Exploited regexp mechanics:

87 bytes using partition

def f(s,c):h,d,t=min(map(s.partition,(c*2).title()));return[s+f'({c})',h+'_'+d+t][d>'']

Attempt This Online!

89 bytes using recursion

f=lambda s,c,h='':s and(s[0]in(c*2).title()and(h+'_'+s)or f(s[1:],c,h+s[0]))or h+f'({c})'

Attempt This Online!

Bash, 99 bytes

for((i=0;i<${#1};i++));{ b=${1:i:1};[[ ${b,} =~ ${2,} ]]&&{ echo ${1/$b/_$b};exit;};}
echo "$1($2)"

Try it online!

Bash, 101 bytes With special characters support

for((i=0;i<${#1};i++));{ b=${1:i:1};[[ ${b,} =~ "${2,}" ]]&&{ echo ${1/$b/_$b};exit;};}
echo "$1($2)"

Try it online!

Java (JDK), 93 bytes

(s,c)->s.matches("(?i).*\\Q"+c+"\\E.*")?s.replaceFirst("(?i)(\\Q"+c+"\\E)","_$1"):s+"("+c+")"

Try it online!

JavaScript (Node.js) , 79 bytes

x=>([a,b]=x.split`, `,V=a.indexOf(b))<0?a+` (${b})`:a.slice(0,V)+'_'+a.slice(V)

Try it online!

Charcoal, 24 bytes

≔⌕↥θ↥ηζ¿⊕ζ«…θζ_✂θζ»«θ(η)

Try it online! Link is to verbose version of code. Explanation:

≔⌕↥θ↥ηζ

Case-insensitively find the position of the accelerator in the string.

¿⊕ζ«

If the accelerator was found, then...

…θζ_✂θζ

Output the first half of the string, then a _, then the second half.

»«θ(η)

Otherwise output the string and the accelerator surrounded with ()s.

Alternative solution, also 24 bytes:

≔⌕↥θ↥ηζ⭆θ⁺…_⁼κζι¿‹ζ⁰⪫()η

Try it online! Link is to verbose version of code. Explanation:

≔⌕↥θ↥ηζ

Case-insensitively find the position of the accelerator in the string.

⭆θ⁺…_⁼κζι

Output the string with the match (if any) preceded with _.

¿‹ζ⁰⪫()η

If there was no match than output the accelerator surrounded with ()s.

Retina 0.8.2, 26 bytes

i`((.).*)¶\2
_$1
¶(.)
($1)

Try it online! Takes input on separate lines but link is to test suite that splits on , for convenience. Explanation:

i`((.).*)¶\2
_$1

If a matching character in the string can be found for the accelerator, precede the match with _ and delete the accelerator.

¶(.)
($1)

If the accelerator has not been deleted then wrap it in ()s and join it to the end of the string.

C (gcc), 71 70 bytes

i;f(s,c){printf(i?"%.*s_%s":"%.*s(%s)",i-s,s,i?:c,i=strcasestr(s,c));}

Try it online!

Perl, 43 bytes

$c=pop;$_=pop;s/\Q$c/_$&/i or$_.="($c)";say
$c=pop;         # retrieve the 2nd argument, the character 
$_=pop;         # retrieve the 1st argument, the string
s/\Q$c/_$&/i      # find the character and add _ before, \Q escapes the character in order to work with .
 or             # OR, if it is not found
$_.="($c)";     # concat the character suround by parenthesis
say             # print the modified string

Perl 5 -pl, 30 bytes

$c=<>;s/\Q$c/_$c/i or$\="($c)"

Try it online!

Japt, 21 bytes

There's gotta be a better way.

i'_Uv bVv¹r"_$""({V})

Try it

i'_Uv bVv¹r"_$""({V})     :Implicit input of strings U & V
i'_                       :Insert "_" in U at index
   Uv                     :  Lowercase U
      b                   :  First 0-based index of, or -1 if not present
       Vv                 :    Lowercase V
         ¹                :End insert
          r               :Replace
           "_$"           :  RegEx /_$/
               "({V})     :  With V interpolated between parentheses

Vyxal, 16 bytes

⇩$⇩₌ḟc[\_Ṁ|_⁰øb+

Try it Online!

Maybe something with assign and functions is shorter, but for now, insert works

Explained

⇩$⇩₌ḟc[\_Ṁ|_⁰øb+­⁡​‎‎⁡⁠⁡‏⁠‎⁡⁠⁢‏⁠‎⁡⁠⁣‏‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁤‏⁠‎⁡⁠⁢⁡‏⁠‎⁡⁠⁢⁢‏‏​⁡⁠⁡‌⁣​‎‎⁡⁠⁢⁣‏‏​⁡⁠⁡‌⁤​‎‎⁡⁠⁢⁤‏⁠‎⁡⁠⁣⁡‏⁠‎⁡⁠⁣⁢‏‏​⁡⁠⁡‌⁢⁡​‎‎⁡⁠⁣⁣‏‏​⁡⁠⁡‌⁢⁢​‎‎⁡⁠⁣⁤‏‏​⁡⁠⁡‌⁢⁣​‎‎⁡⁠⁤⁡‏⁠‎⁡⁠⁤⁢‏⁠‎⁡⁠⁤⁣‏‏​⁡⁠⁡‌⁢⁤​‎‎⁡⁠⁤⁤‏‏​⁡⁠⁡‌­
⇩$⇩               # ‎⁡Push both inputs lowercased. Stack will be [haystack, needle]
   ₌ḟc            # ‎⁢Push haystack.find(needle), needle in haystack
      [           # ‎⁣If needle in haystack (i.e the character is in the string case-insensitive):
       \_Ṁ        # ‎⁤  Insert a "_" at the index (0-indexed)
          |       # ‎⁢⁡Otherwise:
           _      # ‎⁢⁢  Pop the index (which will be -1)
            ⁰øb   # ‎⁢⁣  Surround the character in ()
               +  # ‎⁢⁤  And append it to the sentence
💎

Created with the help of Luminespire.

05AB1E, 22 bytes

DlIlk©diD®è'_ì®ǝë"ÿ(ÿ)

Case-insensitive replacing isn't really 05AB1E's strong suit..

Try it online or verify all test cases.

Explanation:

D                 # Duplicate the first (implicit) input-string
 l                # Convert the copy to lowercase
  Il              # Push the second input-character, as lowercase as well
    k             # Get the first 0-based index of this character in the string
                  # (or -1 if none were present)
     ©            # Store this index in variable `®` (without popping)
      di          # If the index is non-negative (aka, not -1):
        D         #  Duplicate the string on the stack again
         ®è       #  Pop and get the character at index `®`
           '_ì   '#  Prepend a "_"
              ®ǝ  #  Insert it back at index `®`
       ë          # Else:
        "ÿ(ÿ)    "#  Push string "ÿ(ÿ)", where the first `ÿ` is the string on the stack,
                  #  and the second `ÿ` is the (implicit) second input-character
                  # (after which the result is output implicitly)

Google Sheets, 68 bytes

=let(t,regexreplace(A1,"(?i)("&B1&")","_$1"),t&if(t=A1,"("&B1&")",))

screenshot

The formula will "Add an underline before any instance of the char, case-insensitive", as specified, and thus adds multiple underscores when the character appears multiple times.

To only add an underscore before the first instance of the char, use this (74 bytes):

=let(t,regexreplace(A1,"(?i)("&B1&")(.*)","_$1$2"),t&if(t=A1,"("&B1&")",))