diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index b9c8fccde43235b390614c107b64f304b662780d..ae58708aaeafbc1db1c561d7b355c09f91567cfe 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -3247,12 +3247,6 @@ bar
         fail after having already displayed some rows.
         </para>
 
-        <para>
-        <varname>FETCH_COUNT</varname> is ignored if it is unset or does not
-        have a positive value.  It cannot be set to a value that is not
-        syntactically an integer.
-        </para>
-
         <tip>
         <para>
         Although you can use any output format with this feature,
@@ -3316,10 +3310,8 @@ bar
         <term><varname>HISTSIZE</varname></term>
         <listitem>
         <para>
-        The maximum number of commands to store in the command history.
-        If unset, at most 500 commands are stored by default.
-        If set to a value that is negative or not an integer, no limit is
-        applied.
+        The maximum number of commands to store in the command history
+        (default 500).  If set to a negative value, no limit is applied.
         </para>
         <note>
         <para>
@@ -3345,13 +3337,13 @@ bar
         <term><varname>IGNOREEOF</varname></term>
         <listitem>
         <para>
-         If unset, sending an <acronym>EOF</> character (usually
+         If set to 1 or less, sending an <acronym>EOF</> character (usually
          <keycombo action="simul"><keycap>Control</><keycap>D</></>)
          to an interactive session of <application>psql</application>
-         will terminate the application. If set to a numeric value,
-         that many <acronym>EOF</> characters are ignored before the
-         application terminates.  If the variable is set but not to a
-         numeric value, the default is 10.
+         will terminate the application.  If set to a larger numeric value,
+         that many consecutive <acronym>EOF</> characters must be typed to
+         make an interactive session terminate.  If the variable is set to a
+         non-numeric value, it is interpreted as 10.
         </para>
         <note>
         <para>
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 53656294da4a006e5a6201bf7f668edd88609640..3e3cab49418a9e75fd3a6d70d80739dd50fb30ea 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -348,9 +348,9 @@ helpVariables(unsigned short int pager)
 					  "                     (default: 0=unlimited)\n"));
 	fprintf(output, _("  HISTCONTROL        controls command history [ignorespace, ignoredups, ignoreboth]\n"));
 	fprintf(output, _("  HISTFILE           file name used to store the command history\n"));
-	fprintf(output, _("  HISTSIZE           the number of commands to store in the command history\n"));
+	fprintf(output, _("  HISTSIZE           max number of commands to store in the command history\n"));
 	fprintf(output, _("  HOST               the currently connected database server host\n"));
-	fprintf(output, _("  IGNOREEOF          if unset, sending an EOF to interactive session terminates application\n"));
+	fprintf(output, _("  IGNOREEOF          number of EOFs needed to terminate an interactive session\n"));
 	fprintf(output, _("  LASTOID            value of the last affected OID\n"));
 	fprintf(output, _("  ON_ERROR_ROLLBACK  if set, an error doesn't stop a transaction (uses implicit savepoints)\n"));
 	fprintf(output, _("  ON_ERROR_STOP      stop batch execution after error\n"));
diff --git a/src/bin/psql/input.c b/src/bin/psql/input.c
index 3e3e97ad0d22bf5db1671657c1e17de712947a7f..b8c9a00b099885b07eaa22b52b51bf858919945d 100644
--- a/src/bin/psql/input.c
+++ b/src/bin/psql/input.c
@@ -539,10 +539,7 @@ finishInput(void)
 #ifdef USE_READLINE
 	if (useHistory && psql_history)
 	{
-		int			hist_size;
-
-		hist_size = GetVariableNum(pset.vars, "HISTSIZE", 500, -1);
-		(void) saveHistory(psql_history, hist_size);
+		(void) saveHistory(psql_history, pset.histsize);
 		free(psql_history);
 		psql_history = NULL;
 	}
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c
index dc25b4babc5a32f85d457cf082c8bf5382a923e8..6e358e2e1b8470d69b83126864f31d7eea2295c8 100644
--- a/src/bin/psql/mainloop.c
+++ b/src/bin/psql/mainloop.c
@@ -162,7 +162,7 @@ MainLoop(FILE *source)
 				/* This tries to mimic bash's IGNOREEOF feature. */
 				count_eof++;
 
-				if (count_eof < GetVariableNum(pset.vars, "IGNOREEOF", 0, 10))
+				if (count_eof < pset.ignoreeof)
 				{
 					if (!pset.quiet)
 						printf(_("Use \"\\q\" to leave %s.\n"), pset.progname);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 4c7c3b1fa37a0261e5228ece7c8021fac6e1af57..195f5a118438b4a2e8d02df5ed38c56e60bd827a 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -125,6 +125,8 @@ typedef struct _psqlSettings
 	bool		singleline;
 	bool		singlestep;
 	int			fetch_count;
+	int			histsize;
+	int			ignoreeof;
 	PSQL_ECHO	echo;
 	PSQL_ECHO_HIDDEN echo_hidden;
 	PSQL_ERROR_ROLLBACK on_error_rollback;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index a3654e62722cc4201f76078287982f3396b88e96..88d686a5b74e6a81dc53427fd0bc253b9a94559a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -774,6 +774,11 @@ showVersion(void)
  * Substitute hooks and assign hooks for psql variables.
  *
  * This isn't an amazingly good place for them, but neither is anywhere else.
+ *
+ * By policy, every special variable that controls any psql behavior should
+ * have one or both hooks, even if they're just no-ops.  This ensures that
+ * the variable will remain present in variables.c's list even when unset,
+ * which ensures that it's known to tab completion.
  */
 
 static char *
@@ -823,16 +828,71 @@ singlestep_hook(const char *newval)
 	return ParseVariableBool(newval, "SINGLESTEP", &pset.singlestep);
 }
 
+static char *
+fetch_count_substitute_hook(char *newval)
+{
+	if (newval == NULL)
+		newval = pg_strdup("0");
+	return newval;
+}
+
 static bool
 fetch_count_hook(const char *newval)
 {
-	if (newval == NULL)
-		pset.fetch_count = -1;	/* default value */
-	else if (!ParseVariableNum(newval, "FETCH_COUNT", &pset.fetch_count))
-		return false;
+	return ParseVariableNum(newval, "FETCH_COUNT", &pset.fetch_count);
+}
+
+static bool
+histfile_hook(const char *newval)
+{
+	/*
+	 * Someday we might try to validate the filename, but for now, this is
+	 * just a placeholder to ensure HISTFILE is known to tab completion.
+	 */
 	return true;
 }
 
+static char *
+histsize_substitute_hook(char *newval)
+{
+	if (newval == NULL)
+		newval = pg_strdup("500");
+	return newval;
+}
+
+static bool
+histsize_hook(const char *newval)
+{
+	return ParseVariableNum(newval, "HISTSIZE", &pset.histsize);
+}
+
+static char *
+ignoreeof_substitute_hook(char *newval)
+{
+	int			dummy;
+
+	/*
+	 * This tries to mimic the behavior of bash, to wit "If set, the value is
+	 * the number of consecutive EOF characters which must be typed as the
+	 * first characters on an input line before bash exits.  If the variable
+	 * exists but does not have a numeric value, or has no value, the default
+	 * value is 10.  If it does not exist, EOF signifies the end of input to
+	 * the shell."  Unlike bash, however, we insist on the stored value
+	 * actually being a valid integer.
+	 */
+	if (newval == NULL)
+		newval = pg_strdup("0");
+	else if (!ParseVariableNum(newval, NULL, &dummy))
+		newval = pg_strdup("10");
+	return newval;
+}
+
+static bool
+ignoreeof_hook(const char *newval)
+{
+	return ParseVariableNum(newval, "IGNOREEOF", &pset.ignoreeof);
+}
+
 static char *
 echo_substitute_hook(char *newval)
 {
@@ -1062,8 +1122,17 @@ EstablishVariableSpace(void)
 					 bool_substitute_hook,
 					 singlestep_hook);
 	SetVariableHooks(pset.vars, "FETCH_COUNT",
-					 NULL,
+					 fetch_count_substitute_hook,
 					 fetch_count_hook);
+	SetVariableHooks(pset.vars, "HISTFILE",
+					 NULL,
+					 histfile_hook);
+	SetVariableHooks(pset.vars, "HISTSIZE",
+					 histsize_substitute_hook,
+					 histsize_hook);
+	SetVariableHooks(pset.vars, "IGNOREEOF",
+					 ignoreeof_substitute_hook,
+					 ignoreeof_hook);
 	SetVariableHooks(pset.vars, "ECHO",
 					 echo_substitute_hook,
 					 echo_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d6fffcf42f1802f26b12ea2a08242bd6e54e3ae9..6e759d0b76fdf01e84088254160b3297f1b87a1d 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3775,8 +3775,9 @@ append_variable_names(char ***varnames, int *nvars,
 /*
  * This function supports completion with the name of a psql variable.
  * The variable names can be prefixed and suffixed with additional text
- * to support quoting usages. If need_value is true, only the variables
- * that have the set values are picked up.
+ * to support quoting usages. If need_value is true, only variables
+ * that are currently set are included; otherwise, special variables
+ * (those that have hooks) are included even if currently unset.
  */
 static char **
 complete_from_variables(const char *text, const char *prefix, const char *suffix,
@@ -3789,33 +3790,12 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix
 	int			i;
 	struct _variable *ptr;
 
-	static const char *const known_varnames[] = {
-		"AUTOCOMMIT", "COMP_KEYWORD_CASE", "DBNAME", "ECHO", "ECHO_HIDDEN",
-		"ENCODING", "FETCH_COUNT", "HISTCONTROL", "HISTFILE", "HISTSIZE",
-		"HOST", "IGNOREEOF", "LASTOID", "ON_ERROR_ROLLBACK", "ON_ERROR_STOP",
-		"PORT", "PROMPT1", "PROMPT2", "PROMPT3", "QUIET",
-		"SHOW_CONTEXT", "SINGLELINE", "SINGLESTEP",
-		"USER", "VERBOSITY", NULL
-	};
-
 	varnames = (char **) pg_malloc((maxvars + 1) * sizeof(char *));
 
-	if (!need_value)
-	{
-		for (i = 0; known_varnames[i] && nvars < maxvars; i++)
-			append_variable_names(&varnames, &nvars, &maxvars,
-								  known_varnames[i], prefix, suffix);
-	}
-
 	for (ptr = pset.vars->next; ptr; ptr = ptr->next)
 	{
 		if (need_value && !(ptr->value))
 			continue;
-		for (i = 0; known_varnames[i]; i++)		/* remove duplicate entry */
-		{
-			if (strcmp(ptr->name, known_varnames[i]) == 0)
-				continue;
-		}
 		append_variable_names(&varnames, &nvars, &maxvars, ptr->name,
 							  prefix, suffix);
 	}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index 9ca100095f6324bd5ececac9ef335b5bfff0c1a7..d9d07631a59a3730ff31490b4263396ea4ec1119 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -179,31 +179,6 @@ ParseVariableNum(const char *value, const char *name, int *result)
 	}
 }
 
-/*
- * Read integer value of the numeric variable named "name".
- *
- * Return defaultval if it is not set, or faultval if its value is not a
- * valid integer.  (No error message is issued.)
- */
-int
-GetVariableNum(VariableSpace space,
-			   const char *name,
-			   int defaultval,
-			   int faultval)
-{
-	const char *val;
-	int			result;
-
-	val = GetVariable(space, name);
-	if (!val)
-		return defaultval;
-
-	if (ParseVariableNum(val, NULL, &result))
-		return result;
-	else
-		return faultval;
-}
-
 /*
  * Print values of all variables.
  */
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index 84be7805098b996ac7eaa27b720099b39a2fc9d3..19257937c7cba98c8dc9e3453e35e84a7da89f91 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -81,11 +81,6 @@ bool ParseVariableBool(const char *value, const char *name,
 bool ParseVariableNum(const char *value, const char *name,
 				 int *result);
 
-int GetVariableNum(VariableSpace space,
-			   const char *name,
-			   int defaultval,
-			   int faultval);
-
 void		PrintVariables(VariableSpace space);
 
 bool		SetVariable(VariableSpace space, const char *name, const char *value);