First you need to get your shader's output into a Texture2D, so create one of the appropriate size. You'll commonly want ARGB32 format and no mipmaps:
Texture2D outputTex = new Texture2D(width, height, TextureFormat.ARGB32, false)
If the content you want to save has already been rendered by a camera in the scene, you can skip ahead (although in this case Unity's screenshot features may be a shorter route to your goal if you don't need anything fancy)
If you want to use an off-screen buffer instead, we'll need to create a RenderTexture to write it into.
RenderTexture buffer = new RenderTexture(
width,
height,
0, // No depth/stencil buffer
RenderTextureFormat.ARGB32, // Standard colour format
RenderTextureReadWrite.Linear // No sRGB conversions
);
(Assuming you don't need a depth/stencil buffer and don't want to work in sRGB - otherwise, use the parameters that are right for your use case)
Now we bind our RenderTexture and material/shader pass, and run our shader across every pixel.
Graphics.Blit(sourceTexture, buffer, material, passIndex);
If your shader doesn't require texture input, you might need to supply a dummy texture for sourceTexture - I haven't tested how it handles null there. ;)
Now we need to copy the data out of the current render target (whether it was the buffer we created above, or one used internally by a scene camera) and into our outputTex:
RenderTexture.active = buffer; // If not using a scene camera
outputTex.ReadPixels(
new Rect(0, 0, width, height), // Capture the whole texture
0, 0, // Write starting at the top-left texel
false // No mipmaps
);
Now you can use outputTex.EncodeToPNG() and write the results to a file as described in the question. You should use persistentDataPath rather than dataPath if this is something you're doing at runtime though - dataPath is not guaranteed to be a writeable location in a release build.