<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mike Whitaker</title>
    <description>The latest articles on DEV Community by Mike Whitaker (@fleetfootmike).</description>
    <link>https://dev.to/fleetfootmike</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F345824%2Fc69836d7-8c87-4fa7-847b-e31103021dd2.jpeg</url>
      <title>DEV Community: Mike Whitaker</title>
      <link>https://dev.to/fleetfootmike</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fleetfootmike"/>
    <language>en</language>
    <item>
      <title>Shell features you didn't know you needed (or possibly even existed) #9</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Thu, 02 Apr 2026 14:51:13 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/shell-features-you-didnt-know-you-needed-or-possibly-even-existed-9-4g8g</link>
      <guid>https://dev.to/fleetfootmike/shell-features-you-didnt-know-you-needed-or-possibly-even-existed-9-4g8g</guid>
      <description>&lt;h2&gt;
  
  
  Substituting the contents of a variable in Bash.
&lt;/h2&gt;

&lt;p&gt;I came across this one needing to turn a Git branch name into a Docker image tag, and having it trip over the fact that our branch naming convention is &lt;code&gt;&amp;lt;user&amp;gt;/&amp;lt;issue&amp;gt;/&amp;lt;short description&amp;gt;&lt;/code&gt;, and Docker doesn't like &lt;code&gt;/&lt;/code&gt; in tags. &lt;/p&gt;

&lt;p&gt;Obviously there are many ways of stripping/substituting the &lt;code&gt;/&lt;/code&gt;: for example, &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using &lt;code&gt;tr&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;IMAGE_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$BRANCH&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="se"&gt;\/&lt;/span&gt; &lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;using &lt;code&gt;sed&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;IMAGE_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$BRANCH&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/\//-/g'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;using &lt;code&gt;perl&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;IMAGE_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$BRANCH&lt;/span&gt; | perl &lt;span class="nt"&gt;-pe&lt;/span&gt; &lt;span class="s1"&gt;'s/\//-/g'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there's probably others with &lt;code&gt;awk&lt;/code&gt;, and who knows what else.&lt;/p&gt;

&lt;p&gt;But why bother, when you can do it in &lt;code&gt;bash&lt;/code&gt; itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;IMAGE_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BRANCH&lt;/span&gt;&lt;span class="p"&gt;//\//-&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To clarify, that's essentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VARIABLE&lt;/span&gt;&lt;span class="p"&gt;/from/to&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="c"&gt;# replace first occurrence of 'from' &lt;/span&gt;
&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VARIABLE&lt;/span&gt;&lt;span class="p"&gt;//from/to&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="c"&gt;# replace all occurrences of 'from'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, when you know how.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>shell</category>
      <category>regex</category>
    </item>
    <item>
      <title>MariaDB FULLTEXT searches and transactions</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Wed, 01 Apr 2026 11:21:01 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/mariadb-fulltext-searches-and-transactions-552o</link>
      <guid>https://dev.to/fleetfootmike/mariadb-fulltext-searches-and-transactions-552o</guid>
      <description>&lt;p&gt;Another in a series of "things I had to learn the hard way, and you shouldn't".&lt;/p&gt;

&lt;p&gt;The key takeaway from this is that MariaDB FULLTEXT search indices are &lt;strong&gt;not&lt;/strong&gt; updated until a transaction is COMMIT'ed.&lt;/p&gt;

&lt;p&gt;Allow me to demonstrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="n"&gt;affected&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;022&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;FULLTEXT&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; 
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ft_idx&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have a table &lt;code&gt;fulltext_demo&lt;/code&gt; with a row &lt;code&gt;info&lt;/code&gt; indexed for a fulltext search. And just to prove it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"banana"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="n"&gt;affected&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;015&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; 
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;against&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'banana'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;banana&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------+&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far, so good. Now let's do that inside a transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="n"&gt;affected&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"orange"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="n"&gt;affected&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; 
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;against&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orange'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Empty&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If like me, you are going "WTF" at this point, see above. The updated row does not get added to the fulltext search index until the commit. Allow me to demonstrate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="n"&gt;affected&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;MariaDB&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fulltext_demo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt;
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;against&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orange'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;orange&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------+&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes testing things inside a transaction (where you intend to roll back the test fixtures afterwards) just a little bit more irritating than it could be. :D&lt;/p&gt;

</description>
      <category>mariadb</category>
      <category>sql</category>
    </item>
    <item>
      <title>Setting up SSH on Windows</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Mon, 30 Mar 2026 11:04:38 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/setting-up-ssh-on-windows-1dmf</link>
      <guid>https://dev.to/fleetfootmike/setting-up-ssh-on-windows-1dmf</guid>
      <description>&lt;p&gt;Every online tutorial for this seems to be way WAY too complicated for what is in fact a pretty basic process. This is targeted at people who (for example) need to set up a remote SSH session on a Linux server for development purposes).&lt;/p&gt;

&lt;p&gt;We assume you have a username, password and hostname for the server you want to log in it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install OpenSSH on your local Windows machine if it isn't already.
You can find this under Settings, search for "Optional Features"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuoceecc87qmqpgeppdh8.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuoceecc87qmqpgeppdh8.jpeg" alt="OpenSSH" width="545" height="750"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the checkbox, and if it's not installed, install it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fire up a command prompt.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create yourself an SSH key pair, by running &lt;code&gt;ssh-keygen&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;C:\Users\You&amp;gt;&lt;/span&gt;ssh-keygen
&lt;span class="gp"&gt;Generating public/private ed&amp;lt;something&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;key pair.
&lt;span class="gp"&gt;Enter file in which to save the key (C:\Users\You/.ssh/id_ed&amp;lt;something&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;span class="go"&gt;Enter passphrase (empty for no passphrase):
Enter same passphrase again:
&lt;/span&gt;&lt;span class="gp"&gt;Your identification has been saved in C:\Users\You/.ssh/id_ed&amp;lt;something&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="gp"&gt;Your public key has been saved in C:\Users\You/.ssh/id_&amp;lt;something&amp;gt;&lt;/span&gt;.pub
&lt;span class="go"&gt;The key fingerprint is:
&lt;/span&gt;&lt;span class="gp"&gt;SHA256:&amp;lt;some random Text&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;you@YourMachine
&lt;span class="go"&gt;The key's randomart image is:
&lt;/span&gt;&lt;span class="gp"&gt;+--[ED&amp;lt;something&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;256]--+
&lt;span class="go"&gt;| several lines  |
+----[SHA256]-----+****
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a secure public/private key pair. On older machines, it may be saved in &lt;code&gt;.ssh/id_rsa&amp;lt;something&amp;gt;&lt;/code&gt; instead of &lt;code&gt;.ssh/id_ed&amp;lt;something&amp;gt;&lt;/code&gt;. If you have provided a passphrase[1], don't forget it. :D&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Now you need to copy the PUBLIC key (the one ending in &lt;code&gt;.pub&lt;/code&gt;) to your remote server[2]. Run:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;C:\Users\You&amp;gt;&lt;/span&gt;scp .ssh&lt;span class="se"&gt;\i&lt;/span&gt;d&lt;span class="k"&gt;*&lt;/span&gt;.pub user@server:
&lt;span class="gp"&gt;you@server's password: &amp;lt;type your password here&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;   
&lt;/span&gt;&lt;span class="gp"&gt;id_ed&amp;lt;something&amp;gt;&lt;/span&gt;.pub            100%  110     1.9KB/s   00:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the trailing colon on the command line: don't miss this off!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login to the remote server.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;C:\Users\You&amp;gt;&lt;/span&gt;ssh user@server
&lt;span class="gp"&gt;you@server's password: &amp;lt;type your password here&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;&amp;lt;lots of blurb&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;you@server$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;You are now logged into the remote server. Add your ssh public key to the list of permitted ones.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;you@server&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat id&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.pub &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .ssh/authorized_keys
you@server&lt;span class="nv"&gt;$ &lt;/span&gt;&amp;lt;now hit control+D&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will find yourself back at the Windows command prompt&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check you can log in without a password. If you originally provided a passphrase when you ran &lt;code&gt;ssh-keygen&lt;/code&gt;, you will be prompted before you connect.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;C:\Users\You&amp;gt;&lt;/span&gt;ssh user@server
&lt;span class="gp"&gt;&amp;lt;lots of blurb&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;you@server$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[1] The difference between a password and a pass*phrase* is that the former gets transmitted across the network to your remote host, and can potentially be snooped by a malicious attacker. A passphrase is local to your machine, and therefore immune to being snooped.&lt;/p&gt;

&lt;p&gt;[2] You can do this and the next step in one go: the command (on your Windows machine) is&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;type&lt;/span&gt; .ssh&lt;span class="se"&gt;\i&lt;/span&gt;d_&lt;span class="k"&gt;*&lt;/span&gt;.pub&lt;span class="s2"&gt;" | ssh user@host "&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .ssh/authorized_keys&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>beginners</category>
      <category>cli</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Is your TextMate 2 folding corrupt?</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Wed, 21 Jan 2026 22:19:18 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/is-your-textmate-2-folding-corrupt-ha4</link>
      <guid>https://dev.to/fleetfootmike/is-your-textmate-2-folding-corrupt-ha4</guid>
      <description>&lt;p&gt;Having trouble with corrupted/misplaced fold markers in a TextMate 2 file? Has something changed the file under TextMate 2's feet while you have it open with some folds closed? (not looking at anyone in particular, Claude Code :D)&lt;/p&gt;

&lt;p&gt;Simple solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;close the file in TextMate 2&lt;/li&gt;
&lt;li&gt;open Terminal&lt;/li&gt;
&lt;li&gt;run the following:
&lt;code&gt;xattr -d com.macromates.folded &amp;lt;your filename here&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;reopen the file in TextMate 2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Presto. Problem solved.&lt;/p&gt;

</description>
      <category>textmate</category>
      <category>editor</category>
      <category>macintosh</category>
    </item>
    <item>
      <title>Catalyst::Request body issues with the file position pointer</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Fri, 28 Nov 2025 15:15:54 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/catalystrequest-body-issues-with-the-file-position-pointer-2k7l</link>
      <guid>https://dev.to/fleetfootmike/catalystrequest-body-issues-with-the-file-position-pointer-2k7l</guid>
      <description>&lt;p&gt;OK, so...&lt;/p&gt;

&lt;p&gt;For those using the Perl Catalyst web framework in ways involving structured request bodies (e.g. API POSTs)...&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$c-&amp;gt;req-&amp;gt;body&lt;/code&gt; is a string, unless Content-Type is &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;, &lt;code&gt;text/xml&lt;/code&gt;, or &lt;code&gt;multipart/form-data&lt;/code&gt; (or in fact &lt;code&gt;application/json&lt;/code&gt;, which isn't in the docs), in which case it's a &lt;code&gt;File::Temp&lt;/code&gt; (an overloaded file handle), and &lt;code&gt;$c-&amp;gt;req-&amp;gt;body_data&lt;/code&gt; gets you the deserialised body.&lt;/p&gt;

&lt;p&gt;For various reasons, largely to do with the idiosyncrasies of one particular module, I need to read the raw body data from the &lt;code&gt;$c-&amp;gt;req-&amp;gt;body&lt;/code&gt; file handle to process a Stripe webhook payload. For various &lt;em&gt;other&lt;/em&gt; reasons, as part of the API call logging, I need to call &lt;code&gt;$c-&amp;gt;req-&amp;gt;body_data&lt;/code&gt; to get at the deserialised body in another module.&lt;/p&gt;

&lt;p&gt;You may imagine my delight when adding the latter caused the former to fail. &lt;/p&gt;

&lt;p&gt;An afternoon of bad language and extra debug eventually revealed that &lt;code&gt;$c-&amp;gt;req-&amp;gt;body_data&lt;/code&gt; doesn't clean up after itself, and I have to &lt;code&gt;seek( $c-&amp;gt;req-&amp;gt;body, 0, 0)&lt;/code&gt; before I can read any data from the body file handle.&lt;/p&gt;

&lt;p&gt;If this is useful to anyone else, you have my sympathies.&lt;/p&gt;

</description>
      <category>perl</category>
      <category>fileio</category>
      <category>api</category>
      <category>programming</category>
    </item>
    <item>
      <title>How Philips Hue Bridge discovery actually works</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Sat, 06 May 2023 19:01:57 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/how-philips-hue-bridge-discovery-actually-works-1733</link>
      <guid>https://dev.to/fleetfootmike/how-philips-hue-bridge-discovery-actually-works-1733</guid>
      <description>&lt;p&gt;Having been bitten by a change in the Hue discovery protocol a while ago that I've only just got round to figuring out and fixing, I figured it would be useful to stick this up somewhere where Google can find it. So, here we go...&lt;/p&gt;

&lt;p&gt;When a Hue bridge boots up, it has a brief interaction with the Hue servers at &lt;code&gt;meethue.com&lt;/code&gt; and sends it the bridge ID, its IP address and port. This is stored under the IP address that the Hue servers have for you - essentially &lt;code&gt;REMOTE_HOST&lt;/code&gt; in CGI terms (or 'the answer &lt;code&gt;whatismyip.com&lt;/code&gt; gives you').&lt;/p&gt;

&lt;p&gt;When an app (such as the Hue app or the Alexa skill) wants to know where your hub is, it connects to &lt;code&gt;discovery.meethue.com&lt;/code&gt;, which will look at your IP, and if there's a record (or more than one if you have multiple bridges, I guess) for it, returns it/them, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    "id":"001788fff536899d",
    "internalipaddress":"192.168.0.14",
    "port":443
  }
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app then knows it can go find the bridge on the given internal IP, and you're all happily sorted.&lt;/p&gt;

&lt;p&gt;Spotted the issue yet?&lt;/p&gt;

&lt;p&gt;Yup - the wired sockets and one of the Wifi networks on my home network are from the /27 my ISP gave me two decades ago. The Hue is (obviously) on a different IP to my phone, and because there was no NAT involved, the bridge's info was stored on &lt;code&gt;meethue.com&lt;/code&gt; under its &lt;em&gt;actual&lt;/em&gt; routed IP. &lt;/p&gt;

&lt;p&gt;Result, the answer from discovery.meethue.com is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zip. Nada. Discovery failed. &lt;/p&gt;

&lt;p&gt;To quote &lt;code&gt;@tweethue&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That could be the issue. The bridge is designed to be used with a simple home router.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Well, duh.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>philipshue</category>
      <category>nat</category>
    </item>
    <item>
      <title>Upgrading MySQL 5.7 to MariaDB 10 on Ubuntu 18</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Thu, 22 Dec 2022 11:25:56 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/upgrading-mysql-to-mariadb-on-ubuntu-1dja</link>
      <guid>https://dev.to/fleetfootmike/upgrading-mysql-to-mariadb-on-ubuntu-1dja</guid>
      <description>&lt;p&gt;FX: Stands up amid very large pile of yak hair&lt;/p&gt;

&lt;p&gt;Right. If you're ever in the unfortunate space of needing to upgrade MySQL 5.7 to MariaDB 10 on Ubuntu 18.04 LTS, here's what to do and what not to do.&lt;/p&gt;

&lt;p&gt;1) next time, upgrade to 20.04.5 LTS first, and most/all of this will go away :D&lt;/p&gt;

&lt;p&gt;2) do NOT install the default &lt;code&gt;mariadb-server-10.1&lt;/code&gt; package, as its &lt;code&gt;mysql_upgrade&lt;/code&gt; script does &lt;strong&gt;not&lt;/strong&gt; know how to convert from &lt;code&gt;mysql-5.7&lt;/code&gt; tables.&lt;/p&gt;

&lt;p&gt;3) Go &lt;a href="https://mariadb.org/download/?t=repo-config" rel="noopener noreferrer"&gt;here&lt;/a&gt; and select your Ubuntu version and a suitable MariaDB version.&lt;/p&gt;

&lt;p&gt;4) Cut and paste the appropriate repo lines, and do what it says.&lt;/p&gt;

&lt;p&gt;5) if it doesn't run &lt;code&gt;mysql_upgrade&lt;/code&gt; for you, run it as root.&lt;/p&gt;

&lt;p&gt;6) You may find that it hangs on starting the MariaDB server. This is an issue with &lt;code&gt;apparmor&lt;/code&gt;, and can be fixed by following &lt;a href="https://serverfault.com/questions/1013128/mariadb-service-start-stuck-at-activating" rel="noopener noreferrer"&gt;these instructions&lt;/a&gt; (reproduced here just in case of link rot).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop mariadb
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"/usr/sbin/mysqld { }"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apparmor.d/usr.sbin.mysqld
&lt;span class="nb"&gt;sudo &lt;/span&gt;apparmor_parser &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /etc/apparmor.d/usr.sbin.mysqld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above should display &lt;code&gt;Removal succeeded for "/usr/sbin/mysqld"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And then, to stop it reappearing on reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/apparmor.d/usr.sbin.mysqld &lt;span class="se"&gt;\&lt;/span&gt;
/etc/apparmor.d/disable/usr.sbin.mysqld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start mariadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>mysql</category>
      <category>mariadb</category>
      <category>ubuntu</category>
      <category>yakshaving</category>
    </item>
    <item>
      <title>Design your own security vulnerability #2</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Thu, 14 Apr 2022 21:23:59 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/design-your-own-security-vulnerability-2-passwords-4h40</link>
      <guid>https://dev.to/fleetfootmike/design-your-own-security-vulnerability-2-passwords-4h40</guid>
      <description>&lt;p&gt;Passwords. Every CEO's nightmare, surely, is to be woken with the news that their company's password data is up for grabs on the Internet for a price. &lt;/p&gt;

&lt;p&gt;OK - let's get down to brass tacks. How does this happen?&lt;/p&gt;

&lt;p&gt;"Ahah", eager new developer with a brilliant idea for a site thinks. "Let's avoid the pitfalls of &lt;a href="https://dev.to/fleetfootmike/design-your-own-security-vulnerability-1-4hg7"&gt;#1 in this series&lt;/a&gt; and make our users have passwords."&lt;/p&gt;

&lt;p&gt;Fine. Quick bit of SQL and their web framework of choice, pop the user and their password in the appropriate table, and they can go &lt;code&gt;SELECT * FROM users WHERE username="$user" AND password="$password"&lt;/code&gt; and check if it's valid. Simple, right?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sidebar right here: the seasoned veterans screaming at the &lt;em&gt;other&lt;/em&gt; issues with that snippet, just hush. We'll get to that in a couple of articles time, OK? :D&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And here's &lt;strong&gt;problem #1&lt;/strong&gt;. Their database contains the user's unencrypted password. In the event that someone outside the company gains access to the contents of that table, their CEO will be on the phone about 30 seconds after the events in the first paragraph. And they can't assume it won't happen. All the security in the world (and it's a fair bet so far they don't have that!) won't protect them from a disgruntled employee, a social engineering attack, or a misconfigured firewall.&lt;/p&gt;

&lt;p&gt;"Easy", they think. "I'll just encrypt the password, then I can decrypt and compare when the user logs in." And worse, they'll probably invent some really clever (I &lt;em&gt;really&lt;/em&gt; need the sarcasm emoji about now) code to do it.&lt;/p&gt;

&lt;p&gt;And here we are with problems &lt;strong&gt;#2&lt;/strong&gt; and &lt;strong&gt;#3&lt;/strong&gt;! &lt;/p&gt;

&lt;p&gt;Let's deal with #3 first. Rolling your own encryption code. Just. Don't. The odds on our hero coming up with some thing that doesn't have a fundamental flaw with it are small. And if they don't understand the issues involved, what the risks are, and what the encryption function needs to do, their idea of an encryption algorithm &lt;em&gt;might&lt;/em&gt; be a bit better than &lt;code&gt;ROT-13&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;#2 is the bigger problem, and it's an 'ahah' moment when I can get users to realise that it's connected to 'no, I can't tell you your password' and why sites force them to change it if they forgot it. If someone has access to your DB and your code, and therefore the decryption algorithm (which if they have access to your system you better assume they probably do), they have all it takes to crack all your passwords, and we're back to problem #1 and the phone ringing at 4am.&lt;/p&gt;

&lt;p&gt;The key takeaway here is &lt;strong&gt;we don't need to know what the password is&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;We just need to know that what the user typed &lt;strong&gt;encrypts to the same thing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I &lt;em&gt;need&lt;/em&gt; this to be an 'ahah' moment for you if you don't know it already.&lt;/p&gt;

&lt;p&gt;Pick a good one-way algorithm, where turning the password into its 'encrypted' (actually, hashed) form is fast, and breaking the algorithm to get it back is prohibitively hard. Think of it as the algorithmic equivalent of tearing up a piece of paper into lots of pieces - that's quick. Putting it back into one whole piece is not..&lt;/p&gt;

&lt;p&gt;All you have to do, then, is hash the user's password when they create it, pop it in your users table,  and do the same to what they give you when they log in. Compare the two, and bingo.&lt;/p&gt;

&lt;p&gt;But, as a commenter points out, and I did in fact know (insufficient caffeine, yer honour), if we don’t pick our hashing algorithm correctly, we have problem &lt;strong&gt;#4&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;As it stands, two people with the same password have the same hash. Equally, if you know what the algorithm is you can turn up with a table of candidate hashes (basically, run a list of likely passwords through the hash algorithm, which is cheap) and look for matches. &lt;/p&gt;

&lt;p&gt;Solution: pick an algorithm that uses a ‘salt’, a random addition to the password that’s hashed with, and stored with, the hashed password. &lt;/p&gt;

&lt;p&gt;We are firmly in “do not roll your own“ territory here. Any library worth its salt (see what I did there?) here will have both hash and compare functions (e.g. &lt;code&gt;bcrypt&lt;/code&gt; or &lt;code&gt;PKBDF2&lt;/code&gt;) to save you getting it wrong. And maybe now your CEO can rest a little easier on this front.&lt;/p&gt;

&lt;p&gt;Final lesson: don’t code tired, and have someone else review your code before it gets anywhere near production. :) (thanks ARMB) &lt;/p&gt;

&lt;p&gt;As a corollary to this: if a site &lt;em&gt;can&lt;/em&gt; give you your current password back if you've forgotten it? &lt;/p&gt;

&lt;p&gt;Run. &lt;/p&gt;

&lt;p&gt;Very fast. &lt;/p&gt;

&lt;p&gt;In the other direction. &lt;/p&gt;

&lt;p&gt;At the very least do NOT trust it with any data you care about being exposed to a wider audience.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sidebar 2: yes, oh seasoned veterans. This is all an oversimplification. But clearly, some folks - &lt;em&gt;lots&lt;/em&gt; of folks - don't even get this far.&lt;/p&gt;

&lt;p&gt;It is a constant source of wonder and bewilderment to me how often &lt;a href="https://haveibeenpwned.com"&gt;haveibeenpwned&lt;/a&gt; gets updated with new leaked passwords that have clearly been leaked from large, popular sites that don't follow at the minimum these simple best practices. The negligence to let that happen probably should be a criminal offence, and CTOs who presided over it barred from ever holding such an office again.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>security</category>
      <category>sql</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Design your own security vulnerability #1</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Wed, 13 Apr 2022 15:36:40 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/design-your-own-security-vulnerability-1-4hg7</link>
      <guid>https://dev.to/fleetfootmike/design-your-own-security-vulnerability-1-4hg7</guid>
      <description>&lt;p&gt;The first in probably an even more occasional series... None of these are new. But people keep making the same tired old mistakes.&lt;/p&gt;

&lt;p&gt;We begin, as ever, with the best of intentions. As part of our business, let's store some customer data by customer ID, where customer ID is a numeric key on our database of customers. Most databases will happily generate this for you and increment it by one when you add a new customer.&lt;/p&gt;

&lt;p&gt;Let's have a web page so the customer can see his own data, say at &lt;code&gt;https://my-safe-data.biz/customer/NNNNN&lt;/code&gt; where NNNNN is the customer ID. Fine and dandy. No-one but the customer and us know his ID, so he's quite safe. We can just pass that ID to the database, ask it for the data for that customer, and present it. Don't need to password protect it, because the ID is its own password, right - no-one knows anyone else's customer ID, do they, surely?? &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Wrong&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You don't &lt;em&gt;have&lt;/em&gt; to know anyone else's customer ID. Just make an educated guess on how they're generated.&lt;/p&gt;

&lt;p&gt;Enter J Random Cracker, who just happens to have seen one of our URLs because he's a customer. "I wonder", they think, "what happens if I change my customer ID by 1"...&lt;/p&gt;

&lt;p&gt;"Oh look. Someone else's records." An hour and a bit of scripting later, and they've pulled out LOTS of records to which they have no right.&lt;/p&gt;

&lt;p&gt;"But", you say, "no-one would be that stupid, would they?"&lt;/p&gt;

&lt;p&gt;Well yes. &lt;a href="https://www.privacy-ticker.com/data-breach-made-136000-covid-19-test-results-publicly-accessible/?fbclid=IwAR06thzxUwDizuCBTLUhFKjO-4l4VcCa_hHIzcVMgCoXKj24vYFXP04W-_0"&gt;They would&lt;/a&gt; (and yes, just one of &lt;strong&gt;many&lt;/strong&gt; examples, before you ask).&lt;/p&gt;

&lt;p&gt;One easy fix is to require a login before you can access your own data, but that opens more potential vulnerabilities. Of which more later.&lt;/p&gt;

&lt;p&gt;Another is to use a different method of generating customer IDs.&lt;/p&gt;

&lt;p&gt;Same applies.&lt;/p&gt;

</description>
      <category>security</category>
      <category>web</category>
      <category>database</category>
    </item>
    <item>
      <title>Shell command options you didn't know you needed #8</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Wed, 13 Apr 2022 08:15:02 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-8-3df</link>
      <guid>https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-8-3df</guid>
      <description>&lt;p&gt;Another &lt;code&gt;grep&lt;/code&gt; option!&lt;/p&gt;

&lt;p&gt;This also tracks back to &lt;a href="https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-6-7pn"&gt;#6&lt;/a&gt; and &lt;a href="https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-7-3e9a"&gt;#7&lt;/a&gt; in this series, and to be honest, it's more a shell option I really &lt;em&gt;should&lt;/em&gt; have known existed. (I’m not proud: some of the reasoning behind these articles is so that you can learn from my “oh, duh” moments.)&lt;/p&gt;

&lt;p&gt;As you may recall, we're tracking down a bunch of files with &lt;code&gt;iso-8859-1&lt;/code&gt; characters to convert to &lt;code&gt;utf-8&lt;/code&gt;. Things weren't quite as I painted them in the previous posts, as I discovered the directory tree contained a bunch of binary files that I really didn't want to convert. Time to break out the brutal axe and chainsaw combo beloved of shell hackers everywhere, &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;xargs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;find mydir -name '*.html' | xargs grep -l -avx '.*'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Upside, it works.&lt;br&gt;
Massive downside for big directories (mine's probably several million files in a four or five deep tree), that's one process for every smallish group (see &lt;code&gt;xargs --show-limits&lt;/code&gt;) of files. And it's freakin' SLOW (literally hours for me).&lt;/p&gt;

&lt;p&gt;You may imagine me kicking myself (hey, I'm big enough to admit when I mess up!) when I was checking the man page for &lt;code&gt;grep&lt;/code&gt; and found this FAR more elegant solution.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;grep -l -r --include '*.html' -avx '.*' mydir&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--include '&amp;lt;pattern&amp;gt;'&lt;/code&gt; makes &lt;code&gt;grep&lt;/code&gt; only check files that match &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;. As with &lt;code&gt;find -name&lt;/code&gt;, stick the pattern in quotes so the shell doesn't try and match it first. And yes, there's a &lt;code&gt;--exclude&lt;/code&gt; as well :D.&lt;/p&gt;

</description>
      <category>shell</category>
      <category>bash</category>
      <category>unix</category>
      <category>linux</category>
    </item>
    <item>
      <title>Shell command options you didn't know you needed #7</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Tue, 12 Apr 2022 12:26:40 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-7-3e9a</link>
      <guid>https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-7-3e9a</guid>
      <description>&lt;p&gt;&lt;code&gt;grep&lt;/code&gt; has lots of options. This one's really a combination of three to do something nifty. &lt;/p&gt;

&lt;p&gt;On the &lt;a href="https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-6-7pn"&gt;previous post&lt;/a&gt;, I was converting a bunch of &lt;code&gt;iso-8859-1&lt;/code&gt; files to &lt;code&gt;utf-8&lt;/code&gt;. Setting aside for the moment the fact that I was using &lt;code&gt;iconv&lt;/code&gt; for this (read the fine man page!), you might perhaps be wondering, how did I know which files I wanted to convert?&lt;/p&gt;

&lt;p&gt;First off, this is not magic. It presupposes that you know the files are &lt;code&gt;iso-8859-1&lt;/code&gt; and that your locale is set to &lt;code&gt;utf-8&lt;/code&gt;. The latter is easy enough - check your LANG environment variable and set it to something suitable if it doesn't already end in '&lt;code&gt;.UTF-8&lt;/code&gt;' or '&lt;code&gt;.utf8&lt;/code&gt;' (detailed discussion of this not for this article).&lt;/p&gt;

&lt;p&gt;The former is your problem. I can't really help you with this - data is a bunch of bytes plus an encoding you may or may not know :D So. Assuming you have a reasonable level of certainty these files are encoded to &lt;code&gt;iso-8859-1&lt;/code&gt;, run this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;grep -axv '.*' mysuspectfile&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It will return any lines with &lt;code&gt;iso-8859-1&lt;/code&gt; characters that are not legal &lt;code&gt;utf-8&lt;/code&gt;, which is to say pretty much anything with its high bit set. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sidebar: Before anyone jumps on me, yes there are obscure combinations of high-bit-set &lt;code&gt;iso-8859-1&lt;/code&gt; characters that &lt;strong&gt;are&lt;/strong&gt; legal &lt;code&gt;utf-8&lt;/code&gt;, but they are sufficiuently unlikely in any normal text written by people not on weird psychoactive substances for this test to be pretty reliable. Reference &lt;a href="https://stackoverflow.com/questions/22868271/how-to-detect-latin1-and-utf-8"&gt;here&lt;/a&gt; for more details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why does this work?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-a&lt;/code&gt; says 'treat this file as printable text'&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v&lt;/code&gt; says 'invert this match'&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-x&lt;/code&gt; says 'match the whole line against the pattern &lt;code&gt;.*&lt;/code&gt;'&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;.*&lt;/code&gt; (because of &lt;code&gt;-a&lt;/code&gt; and your locale) means 'any legal &lt;code&gt;utf-8&lt;/code&gt; character'. The &lt;code&gt;-x&lt;/code&gt; requires every character in the line to match that pattern, and the &lt;code&gt;-v&lt;/code&gt; will then spit out lines for which that is not the case.&lt;/p&gt;

&lt;p&gt;To generate just a list of offending files from a directory, then, we can use &lt;code&gt;-r&lt;/code&gt; to recurse down the directory, and &lt;code&gt;-l&lt;/code&gt; to just report matching filenames.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;grep -r -l -axv '.*' mydirectory&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Bingo. All ready to throw at &lt;a href="https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-6-7pn"&gt;&lt;code&gt;xargs -P&lt;/code&gt;&lt;/a&gt; :D&lt;/p&gt;

</description>
      <category>shell</category>
      <category>bash</category>
      <category>unix</category>
      <category>linux</category>
    </item>
    <item>
      <title>Shell command options you didn't know you needed #6</title>
      <dc:creator>Mike Whitaker</dc:creator>
      <pubDate>Mon, 11 Apr 2022 16:22:41 +0000</pubDate>
      <link>https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-6-7pn</link>
      <guid>https://dev.to/fleetfootmike/shell-command-options-you-didnt-know-you-needed-6-7pn</guid>
      <description>&lt;p&gt;Been a while, but here's a handy one I discovered over the weekend.&lt;/p&gt;

&lt;p&gt;Back to the trusty &lt;code&gt;xargs&lt;/code&gt;, that rather blunt and brutish chainsaw for processing a long list of files or whatever that someone gave you.&lt;/p&gt;

&lt;p&gt;In this case, I had a list of files that I knew with 99%+ certainty were created on our old server and thus encoded in &lt;code&gt;iso-8859-1&lt;/code&gt;, contained characters that were represented differently in &lt;code&gt;utf-8&lt;/code&gt; (which we had switched to on our new server) and needed converting, and a handy script wrapper around &lt;code&gt;iconv&lt;/code&gt; to do one file at a time. &lt;/p&gt;

&lt;p&gt;All 41,000 of them. The list took four hours to generate, during which time I was pondering the fact that I really should have taken advantage of the fact that, usefully, the new server has 40 cores of Xeon goodness. So we ought to be able to parallel process this list now we've got it, right? And ideally without bothering with GNU Parallel or Perl's &lt;code&gt;Parallel::ForkManager&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Turns out we can!&lt;/p&gt;

&lt;p&gt;&lt;code&gt;xargs -P &amp;lt;n&amp;gt;&lt;/code&gt; (if supported on your OS) runs the commands generated by &lt;code&gt;xargs&lt;/code&gt; in n-way parallel.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cat &amp;lt;list of 41K files&amp;gt; | xargs -n 1 -P 100 &amp;lt;iconv wrapper&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We need the &lt;code&gt;-n 1&lt;/code&gt; as the wrapper only takes one file at a time, and this is how we tell &lt;code&gt;xargs&lt;/code&gt; that. Deep breath. Hit RETURN.&lt;/p&gt;

&lt;p&gt;Whoosh. Load on server briefly rockets to 45, then falls just as fast to its steady 1 and a bit. In about one minute flat, for all 41,000 files.&lt;/p&gt;

&lt;p&gt;Not bad.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>unix</category>
    </item>
  </channel>
</rss>
